diff --git a/.gitignore b/.gitignore index 32b6401..ff2eafa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,11 @@ *.rpm node_modules/ dist/ +cypress/support /*.spec /.vagrant package-lock.json -Test*FAIL* bots/ -test/common/ test/images/ *.pot POTFILES* diff --git a/Makefile b/Makefile index 6bd672c..91e04ab 100644 --- a/Makefile +++ b/Makefile @@ -126,8 +126,8 @@ vm: $(VM_IMAGE) echo $(VM_IMAGE) # run the browser integration tests; skip check for SELinux denials -check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common - TEST_AUDIT_NO_SELINUX=1 test/check-application +check: $(NODE_MODULES_TEST) $(VM_IMAGE) + npm run cypress # checkout Cockpit's bots/ directory for standard test VM images and API to launch them # must be from cockpit's master, as only that has current and existing images; but testvm.py API is stable @@ -136,13 +136,6 @@ bots: git checkout --force FETCH_HEAD -- bots/ git reset bots -# checkout Cockpit's test API; this has no API stability guarantee, so check out a stable tag -# when you start a new project, use the latest relese, and update it from time to time -test/common: - git fetch --depth=1 https://github.com/cockpit-project/cockpit.git 176 - git checkout --force FETCH_HEAD -- test/common - git reset test/common - $(NODE_MODULES_TEST): package.json npm install diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..27b453b --- /dev/null +++ b/cypress.json @@ -0,0 +1,4 @@ +{ + "defaultCommandTimeout": 10000, + "video": false +} diff --git a/cypress/integration/application.js b/cypress/integration/application.js new file mode 100644 index 0000000..ca49840 --- /dev/null +++ b/cypress/integration/application.js @@ -0,0 +1,54 @@ +// Use this for skipping the login page, i. e. all tests which do not test the login page itself +const visit_opts = { auth: { username: 'admin', password: 'foobar' } }; + +describe('Application', () => { + beforeEach('start VM', function () { + cy.task('startVM').then(url => Cypress.config('baseUrl', url)); + + // Programmatically enable the "Reuse my password for privileged tasks" option + cy.server({ + onAnyRequest: function (route, proxy) { + proxy.xhr.setRequestHeader('X-Authorize', 'password'); + } + }); + }); + + afterEach('stop VM', function() { + cy.task('stopVM'); + }); + + it('basic functionality', function () { + // cypress doesn't handle frames, so go to specific frame + cy.visit('/cockpit/@localhost/starter-kit/index.html', visit_opts) + // verify expected heading + cy.get('.container-fluid h2').should('contain', 'Starter Kit'); + // verify expected host name + cy.task('runVM', 'cat /etc/hostname').then(out => { + cy.get('.container-fluid p').should('contain', 'Running on ' + out.trim()); + }); + }); + + it('test with German translations', function() { + cy.visit('/', visit_opts); + + // change language in menu + cy.get('#content-user-name').click(); + cy.get('.display-language-menu a').click(); + cy.get('#display-language select').select('de-de'); + + // HACK: language switching in Chrome not working in current session (Cockpit issue #8160) + cy.on('uncaught:exception', (err, runnable) => { + cy.log("Uncaught exception:", err); + return false; + }); + + // menu label (from manifest) should be translated + cy.get('#display-language-select-button').click(); + cy.get("#host-apps a[href='/starter-kit']").should('contain', 'Bausatz'); + + // page label (from js) should be translated + cy.visit('/cockpit/@localhost/starter-kit/index.html'); + cy.get('.container-fluid p').should('contain', 'Läuft auf'); + }); + +}) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000..9cbbd40 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,69 @@ +const child_process = require("child_process"); + +var vm_proc, ssh_args, cockpit_url; + +// poor man's polling implementation +function waitpid(pid) { + return new Promise((resolve, reject) => { + function check() { + try { + process.kill(pid); + // succeeds → process still exists, poll again + setTimeout(check, 50); + } catch(e) { + // ESRCH → process is gone + if (e.code == "ESRCH") + resolve(null); + else + throw e; + } + } + + check(); + }); +} + +module.exports = (on, config) => { + on("task", { + startVM: image => { + if (!image) + image = process.env.TEST_OS || "fedora-29"; + // already running? happens when cypress restarts the test after visiting a baseUrl the first time + if (vm_proc) + return cockpit_url; + + // no, start a new VM + return new Promise((resolve, reject) => { + let proc = child_process.spawn("bots/machine/testvm.py", [image], + { stdio: ["pipe", "pipe", "inherit"] }); + let buf = ""; + vm_proc = proc.pid; + proc.stdout.on("data", data => { + buf += data.toString(); + if (buf.indexOf("\nRUNNING\n") > 0) { + let lines = buf.split("\n"); + ssh_args = lines[0].split(" ").slice(1); + cockpit_url = lines[1]; + resolve(cockpit_url); + } + }); + proc.on("error", err => reject (err)); + }); + }, + + stopVM: () => { + process.kill(vm_proc); + let p = waitpid(vm_proc); + p.then(() => { vm_proc = null; }); + return p; + }, + + runVM: command => { + res = child_process.spawnSync("ssh", ssh_args.concat(command), + { stdio: ["pipe", "pipe", "inherit"], encoding: "UTF-8" }); + if (res.status) + throw new Error(`Command "${command} failed with code ${res.status}`); + return res.stdout; + } + }); +} diff --git a/package.json b/package.json index 43c1761..7a04bb5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "scripts": { "build": "webpack", "eslint": "eslint --ext .jsx --ext .es6 src/", - "eslint:fix": "eslint --fix --ext .jsx --ext .es6 src/" + "eslint:fix": "eslint --fix --ext .jsx --ext .es6 src/", + "cypress": "cypress run", + "cypress:headed": "cypress run --headed" }, "devDependencies": { "@babel/core": "^7.0.0", @@ -17,10 +19,10 @@ "@babel/preset-react": "^7.0.0", "babel-eslint": "^9.0.0", "babel-loader": "^8.0.0", - "chrome-remote-interface": "^0.26.1", "compression-webpack-plugin": "^1.1.11", "copy-webpack-plugin": "^4.5.2", "css-loader": "^0.28.11", + "cypress": "^3.1.0", "eslint": "^5.4.0", "eslint-config-standard": "^11.0.0", "eslint-config-standard-react": "^6.0.0", @@ -36,7 +38,6 @@ "jed": "^1.1.1", "po2json": "^0.4.5", "sass-loader": "^7.0.3", - "sizzle": "^2.3.3", "stdio": "^0.2.7", "webpack": "^4.17.1", "webpack-cli": "^3.1.0" diff --git a/test/check-application b/test/check-application deleted file mode 100755 index 40431e2..0000000 --- a/test/check-application +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python3 -# Run this with --help to see available options for tracing and debugging -# See https://github.com/cockpit-project/cockpit/blob/master/test/common/testlib.py -# "class Browser" and "class MachineCase" for the available API. - -import os -import sys - -# import Cockpit's machinery for test VMs and its browser test API -TEST_DIR = os.path.dirname(__file__) -sys.path.append(os.path.join(TEST_DIR, "common")) -sys.path.append(os.path.join(os.path.dirname(TEST_DIR), "bots/machine")) -import testlib - - -class TestApplication(testlib.MachineCase): - def testBasic(self): - b = self.browser - m = self.machine - - self.login_and_go("/starter-kit") - # verify expected heading - b.wait_present(".container-fluid h2") - b.wait_text(".container-fluid h2", "Starter Kit") - - # verify expected host name - hostname = m.execute("hostname").strip() - b.wait_present(".container-fluid p") - b.wait_text(".container-fluid p", "Running on " + hostname) - - # change language to German - b.switch_to_top() - b.click("#content-user-name") - b.click(".display-language-menu a") - b.wait_popup('display-language') - b.set_val("#display-language select", "de-de") - b.click("#display-language-select-button") - b.expect_load() - # HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160) - b.reload(ignore_cache=True) - b.wait_present("#content") - # menu label (from manifest) should be translated - b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz") - - b.go("/starter-kit") - b.enter_page("/starter-kit") - # page label (from js) should be translated - b.wait_in_text(".container-fluid p", "Läuft auf") - -if __name__ == '__main__': - testlib.test_main()