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
This commit is contained in:
James Read
2023-10-09 21:44:29 +00:00
committed by GitHub
parent 77b17604f3
commit 2ada67be04
29 changed files with 1632 additions and 1417 deletions
+4 -16
View File
@@ -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
View File
@@ -0,0 +1,8 @@
env:
browser: true
es2021: true
extends: 'eslint:recommended'
parserOptions:
ecmaVersion: 12
sourceType: module
rules: {}
+2
View File
@@ -0,0 +1,2 @@
require:
- mochaSetup.mjs
+4 -5
View File
@@ -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 +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
-14
View File
@@ -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',
},
})
@@ -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
})
})
*/
});
@@ -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
})
})
});
@@ -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
})
})
});
@@ -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
}
@@ -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) => { ... })
-20
View File
@@ -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')
-10
View File
@@ -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
-12
View File
@@ -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
View File
@@ -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()
}
+1382 -1238
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
})
})
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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')