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:
parent
77b17604f3
commit
2ada67be04
|
|
@ -34,28 +34,16 @@ jobs:
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: grpc
|
- name: grpc
|
||||||
run: make grpc
|
run: make -w grpc
|
||||||
|
|
||||||
- name: make daemon
|
- name: make daemon
|
||||||
run: make daemon-compile-x64-lin
|
run: make -w daemon-compile-x64-lin
|
||||||
|
|
||||||
- name: unit tests
|
- name: unit tests
|
||||||
run: make daemon-unittests
|
run: make -w daemon-unittests
|
||||||
|
|
||||||
- name: ls
|
|
||||||
run: ls && pwd
|
|
||||||
|
|
||||||
- name: run OliveTin for integration tests
|
|
||||||
run: ./OliveTin &
|
|
||||||
|
|
||||||
- name: integration tests
|
- name: integration tests
|
||||||
uses: cypress-io/github-action@v6
|
run: cd integration-tests && make -w
|
||||||
with:
|
|
||||||
working-directory: integration-tests
|
|
||||||
spec: cypress/e2e/general/*
|
|
||||||
|
|
||||||
- name: kill OliveTin after integration tests
|
|
||||||
run: killall OliveTin
|
|
||||||
|
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v4.2.0
|
uses: goreleaser/goreleaser-action@v4.2.0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
es2021: true
|
||||||
|
extends: 'eslint:recommended'
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: 12
|
||||||
|
sourceType: module
|
||||||
|
rules: {}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
require:
|
||||||
|
- mochaSetup.mjs
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
cypress:
|
default:
|
||||||
npm install
|
npm install --no-fund
|
||||||
./cypressRun.sh "general"
|
./node_modules/.bin/mocha
|
||||||
./cypressRun.sh "hiddenNav"
|
|
||||||
|
|
||||||
.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
|
||||||
|
|
@ -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) => { ... })
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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/*
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,22 @@
|
||||||
{
|
{
|
||||||
"name": "olivetin-integration-tests",
|
"name": "olivetin-integration-tests",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"repository": "https://github.com/OliveTin/OliveTin-integration-tests",
|
"repository": "https://github.com/OliveTin/OliveTin",
|
||||||
"description": "The cypress WebUI tests",
|
"description": "The integration-tests for OliveTin's webui.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -25,6 +25,7 @@ type webUISettings struct {
|
||||||
func findWebuiDir() string {
|
func findWebuiDir() string {
|
||||||
directoriesToSearch := []string{
|
directoriesToSearch := []string{
|
||||||
cfg.WebUIDir,
|
cfg.WebUIDir,
|
||||||
|
"../webui/",
|
||||||
"/usr/share/OliveTin/webui/",
|
"/usr/share/OliveTin/webui/",
|
||||||
"/var/www/OliveTin/",
|
"/var/www/OliveTin/",
|
||||||
"/var/www/olivetin/",
|
"/var/www/olivetin/",
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<dialog id = "executionResults">
|
<dialog title = "Execution Results" id = "execution-results-popup">
|
||||||
<div class = "action-header">
|
<div class = "action-header">
|
||||||
<span class = "icon" role = "icon"></span>
|
<span class = "icon" role = "icon"></span>
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<template id = "tplArgumentForm">
|
<template id = "tplArgumentForm">
|
||||||
<dialog>
|
<dialog title = "Arguments" id = "argument-popup">
|
||||||
<form class = "action-arguments">
|
<form class = "action-arguments">
|
||||||
<div class = "wrapper">
|
<div class = "wrapper">
|
||||||
<div class = "action-header">
|
<div class = "action-header">
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class ActionButton extends window.HTMLElement {
|
||||||
this.btn.title = json.title
|
this.btn.title = json.title
|
||||||
this.btn.onclick = () => {
|
this.btn.onclick = () => {
|
||||||
if (json.arguments.length > 0) {
|
if (json.arguments.length > 0) {
|
||||||
|
for (const oldArgumentForm of document.querySelectorAll('argument-form')) {
|
||||||
|
oldArgumentForm.remove()
|
||||||
|
}
|
||||||
|
|
||||||
const frm = document.createElement('argument-form')
|
const frm = document.createElement('argument-form')
|
||||||
frm.setup(json, (args) => {
|
frm.setup(json, (args) => {
|
||||||
this.startAction(args)
|
this.startAction(args)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export class ExecutionDialog {
|
||||||
constructFromJson (json) {
|
constructFromJson (json) {
|
||||||
this.executionUuid = 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.domIcon = this.dlg.querySelector('.icon')
|
||||||
this.domTitle = this.dlg.querySelector('.title')
|
this.domTitle = this.dlg.querySelector('.title')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue