瀏覽代碼

Migrate integration-test infrastucture from Cypress to Selenium+Mocha (#170)

* cicd: Move cypress to selenium+mocha

* cicd: with is not supported unless using an a container

* cicd: relative path to mocha

* cicd: the integration-tests runner now starts/stops OliveTin

* cicd: Knowing the CWD helps debugging

* cicd: Headless chrome

* cicd: wait for integration-test server to be started

* fmt: Mostly fix isses from eslint

* cicd: #169 - Test multiple combo boxes

* fmt: let should be const

* cicd: Remove cypress entirely
James Read 2 年之前
父節點
當前提交
2ada67be04

+ 4 - 16
.github/workflows/build-snapshot.yml

@@ -34,28 +34,16 @@ jobs:
           cache: true
 
       - name: grpc
-        run: make grpc
+        run: make -w grpc
 
       - name: make daemon
-        run: make daemon-compile-x64-lin
+        run: make -w daemon-compile-x64-lin
 
       - name: unit tests
-        run: make daemon-unittests
-
-      - name: ls
-        run: ls && pwd
-
-      - name: run OliveTin for integration tests
-        run: ./OliveTin &
+        run: make -w daemon-unittests
 
       - name: integration tests
-        uses: cypress-io/github-action@v6
-        with:
-          working-directory: integration-tests
-          spec: cypress/e2e/general/*
-
-      - name: kill OliveTin after integration tests
-        run: killall OliveTin
+        run: cd integration-tests && make -w
 
       - name: goreleaser
         uses: goreleaser/goreleaser-action@v4.2.0

+ 8 - 0
integration-tests/.eslintrc.yml

@@ -0,0 +1,8 @@
+env:
+  browser: true
+  es2021: true
+extends: 'eslint:recommended'
+parserOptions:
+  ecmaVersion: 12
+  sourceType: module
+rules: {}

+ 2 - 0
integration-tests/.mocharc.yml

@@ -0,0 +1,2 @@
+require:
+  - mochaSetup.mjs

+ 4 - 5
integration-tests/Makefile

@@ -1,6 +1,5 @@
-cypress:
-	npm install
-	./cypressRun.sh "general"
-	./cypressRun.sh "hiddenNav"
+default:
+	npm install --no-fund
+	./node_modules/.bin/mocha
 
-.PHONY: cypress container
+.PHONY: default

+ 0 - 0
integration-tests/configs/config.general.yaml → integration-tests/configs/general/config.yaml


+ 0 - 0
integration-tests/configs/config.hiddenFooter.yaml → integration-tests/configs/hiddenFooter/config.yaml


+ 0 - 0
integration-tests/configs/config.hiddenNav.yaml → integration-tests/configs/hiddenNav/config.yaml


+ 25 - 0
integration-tests/configs/multipleDropdowns/config.yaml

@@ -0,0 +1,25 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+- title: Ping Google.com
+  shell: ping google.com -c 1
+  icon: ping
+
+- title: Test multiple dropdowns
+  shell: echo {{ salutation }} {{ person }}
+  icon: ping
+  arguments:
+    - name: salutation
+      choices:
+        - value: Hello
+        - value: Goodbye
+
+    - name: person
+      choices:
+        - value: Alice
+        - value: Bob
+        - value: Dave

+ 0 - 14
integration-tests/cypress.config.js

@@ -1,14 +0,0 @@
-const { defineConfig } = require('cypress')
-
-module.exports = defineConfig({
-  screenshotsFolder: 'results/screenshots/',
-  videosFolder: 'results/videos/',
-  e2e: {
-    // We've imported your old cypress plugins here.
-    // You may want to clean this up later by importing these.
-    setupNodeEvents(on, config) {
-      return require('./cypress/plugins/index.js')(on, config)
-    },
-    baseUrl: 'http://localhost:1337',
-  },
-})

+ 0 - 21
integration-tests/cypress/e2e/general/defaultHomepageRender.cy.js

@@ -1,21 +0,0 @@
-describe('Homepage rendering', () => {
-  beforeEach(() => {
-    cy.visit("/")
-  });
-
-  it("Footer contains promo", () => {
-    cy.get('footer').contains("OliveTin")
-  })
-
-  it('Default buttons are rendered', () => {
-    cy.get("#root-group button").should('have.length', 8)
-  })
-
-  /*
-  it('Switcher navigation is visible', () => {
-    cy.get('#section-switcher').then($el => {
-      expect(Cypress.dom.isHidden($el)).to.be.false
-    })
-  })
-  */
-});

+ 0 - 12
integration-tests/cypress/e2e/hiddenFooter/hiddenFooter.cy.js

@@ -1,12 +0,0 @@
-describe('Hidden Footer', () => {
-  beforeEach(() => {
-    cy.visit("/")
-    cy.wait(500)
-  });
-
-  it('Footer is hidden', () => {
-    cy.get('footer').then($el => {
-      expect(Cypress.dom.isHidden($el)).to.be.true
-    })
-  })
-});

+ 0 - 16
integration-tests/cypress/e2e/hiddenNav/hiddenNav.cy.js

@@ -1,16 +0,0 @@
-describe('Hidden Nav', () => {
-  beforeEach(() => {
-    cy.visit("/")
-    cy.wait(500)
-  });
-
-  it("Footer contains promo", () => {
-    cy.get('footer').contains("OliveTin")
-  })
-
-  it('Switcher navigation is hidden', () => {
-    cy.get('#section-switcher').then($el => {
-      expect(Cypress.dom.isHidden($el)).to.be.true
-    })
-  })
-});

+ 0 - 22
integration-tests/cypress/plugins/index.js

@@ -1,22 +0,0 @@
-/// <reference types="cypress" />
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
-
-/**
- * @type {Cypress.PluginConfig}
- */
-// eslint-disable-next-line no-unused-vars
-module.exports = (on, config) => {
-  // `on` is used to hook into various events Cypress emits
-  // `config` is the resolved Cypress config
-}

+ 0 - 25
integration-tests/cypress/support/commands.js

@@ -1,25 +0,0 @@
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add('login', (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

+ 0 - 20
integration-tests/cypress/support/e2e.js

@@ -1,20 +0,0 @@
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
-import './commands'
-
-// Alternatively you can use CommonJS syntax:
-// require('./commands')

+ 0 - 10
integration-tests/cypressRun.sh

@@ -1,10 +0,0 @@
-#!/bin/bash
-
-set -o xtrace
-
-echo "Running config $1"
-
-cp -f ./configs/config.$1.yaml ./configs/config.yaml
-docker start olivetin
-NO_COLOR=1 ./node_modules/.bin/cypress run --headless -s cypress/integration/$1/*  || true
-docker kill olivetin

+ 0 - 12
integration-tests/cypressRunVagrant.sh

@@ -1,12 +0,0 @@
-#!/bin/bash
-
-# args:
-# $1: The Vagrant VM to test against. If blank and only one VM is provisioned, it will use that.
-
-IP=$(vagrant ssh-config $1 | grep HostName | awk '{print $2}')
-BASE_URL="http://$IP:1337/"
-
-echo "IP: $IP, BaseURL: $BASE_URL"
-
-# Only run the general test, as we cannot easily switch out configs in VMs yet.
-./node_modules/.bin/cypress run --headless -c baseUrl=$BASE_URL -s cypress/e2e/general/*

+ 18 - 0
integration-tests/mochaSetup.mjs

@@ -0,0 +1,18 @@
+import { Options } from 'selenium-webdriver/chrome.js'
+import { Builder, Browser } from 'selenium-webdriver'
+import getRunner from './runner.mjs'
+
+export async function mochaGlobalSetup () {
+  const options = new Options()
+  options.addArguments('--headless')
+
+  global.webdriver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build()
+
+  global.runner = getRunner()
+
+  console.log("Runner constructor: " + global.runner.constructor.name)
+}
+
+export async function mochaGlobalTeardown () {
+  await global.webdriver.quit()
+}

文件差異過大導致無法顯示
+ 670 - 559
integration-tests/package-lock.json


+ 10 - 3
integration-tests/package.json

@@ -1,15 +1,22 @@
 {
   "name": "olivetin-integration-tests",
   "version": "1.0.0",
-  "repository": "https://github.com/OliveTin/OliveTin-integration-tests",
-  "description": "The cypress WebUI tests",
+  "repository": "https://github.com/OliveTin/OliveTin",
+  "description": "The integration-tests for OliveTin's webui.",
   "main": "index.js",
+  "type": "module",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "",
   "license": "ISC",
   "devDependencies": {
-    "cypress": "^13.2.0"
+    "chai": "^4.3.10",
+    "eslint": "^8.51.0",
+    "mocha": "^10.2.0",
+    "selenium-webdriver": "4.12.0"
+  },
+  "dependencies": {
+    "wait-on": "^7.0.1"
   }
 }

+ 61 - 0
integration-tests/runner.mjs

@@ -0,0 +1,61 @@
+import process from 'node:process'
+import waitOn from 'wait-on'
+import { spawn } from 'node:child_process'
+
+let ot = null
+
+export default function getRunner () {
+  const type = process.env.OLIVETIN_TEST_RUNNER
+
+  console.log('OLIVETIN_TEST_RUNNER env value is: ', type)
+
+  switch (type) {
+    case 'local':
+      return new OliveTinTestRunnerLocalProcess()
+    case 'vm':
+      return null
+    case 'container':
+      return null
+    default:
+      return new OliveTinTestRunnerLocalProcess()
+  }
+}
+
+class OliveTinTestRunnerLocalProcess {
+  async start (cfg) {
+    ot = spawn('./../OliveTin', ['-configdir', 'configs/' + cfg + '/'])
+
+    const logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
+
+    if (logStdout) {
+      ot.stdout.on('data', (data) => {
+        console.log(`stdout: ${data}`)
+      })
+
+      ot.stderr.on('data', (data) => {
+        console.error(`stderr: ${data}`)
+      })
+    }
+
+    ot.on('close', (code) => {
+      if (code != null) {
+        console.log(`child process exited with code ${code}`)
+      }
+    })
+
+    /*
+      this.server = await startSomeServer({port: process.env.TEST_PORT});
+      console.log(`server running on port ${this.server.port}`);
+      */
+
+    await waitOn({
+      'resources': ['http://localhost:1337/']
+    })
+
+    return ot
+  }
+
+  async stop () {
+    await ot.kill()
+  }
+}

+ 35 - 0
integration-tests/test/general.mjs

@@ -0,0 +1,35 @@
+import {expect} from 'chai';
+import {By} from 'selenium-webdriver';
+
+describe('config: general', function () {
+  before(async function () {
+    await runner.start('general')
+  });
+
+  after(async () => {
+    await runner.stop()
+  });
+
+  it('Page title', async function () {
+    await webdriver.get('http://localhost:1337')
+
+    let title = await webdriver.getTitle();
+    expect(title).to.be.equal("OliveTin")
+  })
+
+  it('Footer contains promo', async function () {
+    let ftr = await webdriver.findElement(By.tagName('footer')).getText()
+
+    expect(ftr).to.contain("Documentation")
+  })
+
+  it('Default buttons are rendered', async function() {
+    await webdriver.get('http://localhost:1337')
+
+//    await webdriver.manage().setTimeouts({ implicit: 2000 });
+
+    let buttons = await webdriver.findElement(By.id('root-group')).findElements(By.tagName('button'))
+
+    expect(buttons).to.have.length(6);
+  })
+})

+ 21 - 0
integration-tests/test/hiddenFooter.mjs

@@ -0,0 +1,21 @@
+import { expect } from 'chai';
+
+import { By } from 'selenium-webdriver';
+
+describe('config: hiddenFooter', function () {
+  before(async function () {
+    await runner.start('hiddenFooter')
+  });
+
+  after(async () => {
+    await runner.stop()
+  });
+
+  it('Check that footer is hidden', async () => {
+    await webdriver.get('http://localhost:1337')
+
+    let footer = await webdriver.findElement(By.tagName('footer'))
+
+    expect(await footer.isDisplayed()).to.be.false
+  })
+})

+ 20 - 0
integration-tests/test/hiddenNav.mjs

@@ -0,0 +1,20 @@
+import {expect} from 'chai';
+import {By} from 'selenium-webdriver';
+
+describe('config: hiddenNav', function () {
+  before(async function () {
+    await runner.start('hiddenNav')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  it('nav is hidden', async () => {
+    await webdriver.get('http://localhost:1337')
+
+    const toggler = await webdriver.findElement(By.id('sidebar-toggle-wrapper'))
+
+    expect(await toggler.isDisplayed()).to.be.false
+  })
+})

+ 34 - 0
integration-tests/test/multipleDropdowns.js

@@ -0,0 +1,34 @@
+import { expect } from 'chai'
+import { By, until } from 'selenium-webdriver'
+import fs from 'node:fs'
+
+describe('config: multipleDropdowns', function () {
+  before(async function () {
+    await runner.start('multipleDropdowns')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  it('Multiple dropdowns are possible', async function() {
+    await webdriver.get('http://localhost:1337')
+    await webdriver.manage().setTimeouts({ implicit: 2000 });
+
+    const button = await webdriver.findElement(By.id('actionButton_bdc45101bbd12c1397557790d9f3e059')).findElement(By.tagName('button'));
+
+    expect(button).to.not.be.undefined;
+
+    await button.click()
+
+    const dialog = await webdriver.findElement(By.id('argument-popup'));
+
+    await webdriver.wait(until.elementIsVisible(dialog), 2000)
+
+    const selects = await dialog.findElements(By.tagName('select'))
+
+    expect(selects).to.have.length(2)
+    expect(await selects[0].findElements(By.tagName('option'))).to.have.length(2)
+    expect(await selects[1].findElements(By.tagName('option'))).to.have.length(3)
+  })
+})

+ 1 - 0
internal/httpservers/webuiServer.go

@@ -25,6 +25,7 @@ type webUISettings struct {
 func findWebuiDir() string {
 	directoriesToSearch := []string{
 		cfg.WebUIDir,
+		"../webui/",
 		"/usr/share/OliveTin/webui/",
 		"/var/www/OliveTin/",
 		"/var/www/olivetin/",

+ 2 - 2
webui/index.html

@@ -70,7 +70,7 @@
 			</p>
 		</footer>
 
-		<dialog id = "executionResults">
+		<dialog title = "Execution Results" id = "execution-results-popup">
 			<div class = "action-header">
 				<span class = "icon" role = "icon"></span>
 
@@ -110,7 +110,7 @@
 		</dialog>
 
 		<template id = "tplArgumentForm">
-			<dialog>
+			<dialog title = "Arguments" id = "argument-popup">
 				<form class = "action-arguments">
 					<div class = "wrapper">
 						<div class = "action-header">

+ 4 - 0
webui/js/ActionButton.js

@@ -19,6 +19,10 @@ class ActionButton extends window.HTMLElement {
     this.btn.title = json.title
     this.btn.onclick = () => {
       if (json.arguments.length > 0) {
+        for (const oldArgumentForm of document.querySelectorAll('argument-form')) {
+          oldArgumentForm.remove()
+        }
+
         const frm = document.createElement('argument-form')
         frm.setup(json, (args) => {
           this.startAction(args)

+ 1 - 1
webui/js/ExecutionDialog.js

@@ -5,7 +5,7 @@ export class ExecutionDialog {
   constructFromJson (json) {
     this.executionUuid = json
 
-    this.dlg = document.querySelector('dialog#executionResults')
+    this.dlg = document.querySelector('dialog#execution-results-popup')
 
     this.domIcon = this.dlg.querySelector('.icon')
     this.domTitle = this.dlg.querySelector('.title')

部分文件因文件數量過多而無法顯示