From 40aa793695a8f06f124963669e9e31093c5fa02c Mon Sep 17 00:00:00 2001 From: Steve Tynor Date: Thu, 11 Jun 2020 17:21:29 -0400 Subject: [PATCH 1/5] add simple spectron-based integration test in uitest --- main.go | 36 +++++++++++++- uitest/.gitignore | 3 ++ uitest/README.md | 23 +++++++++ uitest/config.js | 9 ++++ uitest/package.json | 18 +++++++ uitest/test/hooks.js | 106 +++++++++++++++++++++++++++++++++++++++++ uitest/test/mocha.opts | 1 + uitest/test/test.js | 30 ++++++++++++ 8 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 uitest/.gitignore create mode 100644 uitest/README.md create mode 100644 uitest/config.js create mode 100644 uitest/package.json create mode 100644 uitest/test/hooks.js create mode 100644 uitest/test/mocha.opts create mode 100644 uitest/test/test.js diff --git a/main.go b/main.go index 60d41cb..1b1068f 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ import ( "flag" "fmt" "log" + "os/exec" + "strings" "time" "github.com/asticode/go-astikit" @@ -26,8 +28,9 @@ var ( // Application Vars var ( - debug = flag.Bool("d", true, "enables the debug mode") - w *astilectron.Window + uitest = flag.Int("UITEST", 0, "if non-zero, the port that the uitest will use to attach to the main process's listener") + debug = flag.Bool("d", true, "enables the debug mode") + w *astilectron.Window ) func main() { @@ -37,9 +40,34 @@ func main() { // Create logger l := log.New(log.Writer(), log.Prefix(), log.Flags()) + /// these are only overrridden if the -UITEST flag passed an alternate port + var executer = astilectron.DefaultExecuter + var acceptTimeout = astilectron.DefaultAcceptTCPTimeout + var adapter bootstrap.AstilectronAdapter = nil + var astiPort = 0 + + if *uitest != 0 { + astiPort = *uitest + + executer = func(l astikit.SeverityLogger, a *astilectron.Astilectron, cmd *exec.Cmd) (err error) { + // We wait for the test framework to start the renderer process + l.Infof("======= Waiting for test framework to start %s\n", strings.Join(cmd.Args, " ")) + return + } + + // give the test framework plenty of time to startup + acceptTimeout = time.Minute * 3 + + adapter = func(a *astilectron.Astilectron) { + // configure astilectron to not start the renderer process; let the test framework attach itself + a.SetExecuter(executer) + } + } + // Run bootstrap l.Printf("Running app built at %s\n", BuiltAt) if err := bootstrap.Run(bootstrap.Options{ + Adapter: adapter, // used to coordinate the alternate startup used by the UITEST Asset: Asset, AssetDir: AssetDir, AstilectronOptions: astilectron.Options{ @@ -49,6 +77,10 @@ func main() { SingleInstance: true, VersionAstilectron: VersionAstilectron, VersionElectron: VersionElectron, + + // for UITEST support: + TCPPort: &astiPort, + AcceptTCPTimeout: acceptTimeout, }, Debug: *debug, Logger: l, diff --git a/uitest/.gitignore b/uitest/.gitignore new file mode 100644 index 0000000..8800b68 --- /dev/null +++ b/uitest/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +screenshots*png diff --git a/uitest/README.md b/uitest/README.md new file mode 100644 index 0000000..eac1f3e --- /dev/null +++ b/uitest/README.md @@ -0,0 +1,23 @@ +A sample NodeJS / Spectron-based end-to-end test for the Astilectron demo + +# Prerequisites + +* Install npm (see [https://www.npmjs.com/get-npm]). + +# Step 1: install nodejs and the test dependencies + +Run the following command: + $ npm install + +# Step 2: run the demo at least once after building it (to allow it to provision the electron artifacts) + +# Step 3: run the test: + +Run the following command: + $ npm test + +# Troubleshooting + +* If you get an error suggesting that the test can't find the electron executable, ensure you run the demo at least once to allow astilectron to provision the electron artifacts + +* "Uncaught javascript exception" ECONNRESET: \ No newline at end of file diff --git a/uitest/config.js b/uitest/config.js new file mode 100644 index 0000000..9854a4f --- /dev/null +++ b/uitest/config.js @@ -0,0 +1,9 @@ +const config = { + default: { + url: 'https://duckduckgo.com' + }, +}; + +exports.get = function get(env) { + return config[env] || config.default; +}; \ No newline at end of file diff --git a/uitest/package.json b/uitest/package.json new file mode 100644 index 0000000..8beaa73 --- /dev/null +++ b/uitest/package.json @@ -0,0 +1,18 @@ +{ + "name": "astilectron-demo-uitest", + "version": "1.0.0", + "description": "Selenium/Spectron based UI automation to test astilectron demo", + "scripts": { + "test": "node ./node_modules/mocha/bin/_mocha test/test.js" + }, + "devDependencies": { + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "electron": "^6.0.12", + "mocha": "^5.2.0", + "spectron": "^9.0.0" + }, + "engines": { + "node": "^8.12.0" + } +} diff --git a/uitest/test/hooks.js b/uitest/test/hooks.js new file mode 100644 index 0000000..8b2017c --- /dev/null +++ b/uitest/test/hooks.js @@ -0,0 +1,106 @@ +const Application = require('spectron').Application; +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const electron = require('electron'); +const { exec } = require("child_process"); + +const APPNAME = 'Astilectron demo'; +const PORT = 55555; // the port the main process will listen to + +global.before(() => { + chai.should(); + chai.use(chaiAsPromised); +}); + +// Map nodejs arch to golang arch +let archMap = new Map([ + ["arm", "arm"], + ["x86", "win32"], + ["x64", "amd64"], +]); + +function mainExe() { + if (process.platform === 'darwin') { + return `../output/darwin-amd64/${APPNAME}.app/Contents/MacOS/${APPNAME}`; + } else if (process.platform === 'linux') { + return `../output/linux-${archMap[process.arch]}/${APPNAME}`; + } else if (process.platform === 'win32') { + return `../output/windows-${archMap[process.arch]}/${APPNAME}.exe`; + } else { + console.log("FATAL: unhandled platform - add your variant here"); + process.exit(1); + } +} + +function electronExe() { + if (process.platform === 'darwin') { + return `../output/darwin-amd64/${APPNAME}.app/Contents/MacOS/vendor/electron-darwin-amd64/${APPNAME}.app/Contents/MacOS/${APPNAME}`; + } else if (process.platform === 'linux') { + return `../output/linux-${archMap[process.arch]}/vendor/electron-linux-${archMap[process.arch]}/electron`; + } else if (process.platform === 'win32') { + return `${process.env.APPDATA}/Roaming/${APPNAME}/vendor/electron-windows-${archMap[process.arch]}/Electron.exe`; + } else { + console.log("FATAL: unhandled platform - add your variant here"); + process.exit(1); + } +} + +function astilectronJS() { + if (process.platform === 'darwin') { + return `../output/darwin-amd64/${APPNAME}.app/Contents/MacOS/vendor/astilectron/main.js`; + } else if (process.platform === 'linux') { + return `../output/linux-${archMap[process.arch]}/vendor/vendor/astilectron/main.js`; + } else if (process.platform === 'win32') { + return `${process.env.APPDATA}/Roaming/${APPNAME}/vendor/astilectron/main.js`; + } else { + console.log("FATAL: unhandled platform - add your variant here"); + process.exit(1); + } +} + +module.exports = { + async startMainApp() { + console.log(`Starting main exe: ${mainExe()}`); + exec(`"${mainExe()}" -UITEST ${PORT}`, (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + return; + } + console.log(`stdout: ${stdout}`); + + }); + }, + + async getApp() { + return module.exports.app; + }, + + async startApp() { + module.exports.startMainApp(); + + console.log(`Starting electron exe: ${electronExe()}`); + const rendererApp = await new Application({ + + path: electronExe(), + args: [astilectronJS(), `127.0.0.1:${PORT}`, 'true'], + + // for debugging: + //chromeDriverLogPath: './chromedriver.log', + //webdriverLogPath: './webdriver.log' + + }).start(); + chaiAsPromised.transferPromiseness = rendererApp.transferPromiseness; + module.exports.app = rendererApp; + return rendererApp; + }, + + async stopApp(app) { + if (app && app.isRunning()) { + await app.stop(); + } + } +}; diff --git a/uitest/test/mocha.opts b/uitest/test/mocha.opts new file mode 100644 index 0000000..0689afd --- /dev/null +++ b/uitest/test/mocha.opts @@ -0,0 +1 @@ +--timeout 20000 diff --git a/uitest/test/test.js b/uitest/test/test.js new file mode 100644 index 0000000..24060f3 --- /dev/null +++ b/uitest/test/test.js @@ -0,0 +1,30 @@ +const hooks = require('./hooks'); +const config = require('../config').get(process.env.NODE_ENV); + +var app; + +describe('Setup', () => { + + before(async () => { + app = await hooks.startApp(); + }); + + after(async () => { + await hooks.stopApp(app); + }); + + it('opens a window', async () => { + await app.client + .waitUntilWindowLoaded() + .getWindowCount() + .should.eventually.be.above(0) + + .getTitle().should.eventually.equal('') // the demo doesnt set a title + }); + + it('finds some files', async () => { + await app.client + .getText('#files_count').should.eventually.not.be.equal('') + }); + +}); From 00f6f9a15d652835220d316152176c48d4edeedc Mon Sep 17 00:00:00 2001 From: Steve Tynor Date: Thu, 11 Jun 2020 17:52:51 -0400 Subject: [PATCH 2/5] tested on windows --- uitest/test/hooks.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/uitest/test/hooks.js b/uitest/test/hooks.js index 8b2017c..d797e0e 100644 --- a/uitest/test/hooks.js +++ b/uitest/test/hooks.js @@ -13,11 +13,18 @@ global.before(() => { }); // Map nodejs arch to golang arch -let archMap = new Map([ - ["arm", "arm"], - ["x86", "win32"], - ["x64", "amd64"], -]); +let archMap = { + "arm": "arm", + "ia32": "386", + "x86": "386", + "x64": "amd64", + "ia64": "amd64" +}; + +if (archMap[process.arch] === undefined) { + console.log(`FATAL: unhandled platform/processor type (${process.arch}) - add your variant to archMap in test/hooks.js`); + process.exit(1); +} function mainExe() { if (process.platform === 'darwin') { @@ -27,7 +34,7 @@ function mainExe() { } else if (process.platform === 'win32') { return `../output/windows-${archMap[process.arch]}/${APPNAME}.exe`; } else { - console.log("FATAL: unhandled platform - add your variant here"); + console.log("FATAL: unhandled platform/os - add your variant here"); process.exit(1); } } @@ -38,7 +45,7 @@ function electronExe() { } else if (process.platform === 'linux') { return `../output/linux-${archMap[process.arch]}/vendor/electron-linux-${archMap[process.arch]}/electron`; } else if (process.platform === 'win32') { - return `${process.env.APPDATA}/Roaming/${APPNAME}/vendor/electron-windows-${archMap[process.arch]}/Electron.exe`; + return `${process.env.APPDATA}/${APPNAME}/vendor/electron-windows-${archMap[process.arch]}/Electron.exe`; } else { console.log("FATAL: unhandled platform - add your variant here"); process.exit(1); @@ -51,7 +58,7 @@ function astilectronJS() { } else if (process.platform === 'linux') { return `../output/linux-${archMap[process.arch]}/vendor/vendor/astilectron/main.js`; } else if (process.platform === 'win32') { - return `${process.env.APPDATA}/Roaming/${APPNAME}/vendor/astilectron/main.js`; + return `${process.env.APPDATA}/${APPNAME}/vendor/astilectron/main.js`; } else { console.log("FATAL: unhandled platform - add your variant here"); process.exit(1); @@ -60,6 +67,7 @@ function astilectronJS() { module.exports = { async startMainApp() { + console.log(`node arch: "${process.arch}" golang arch: "${archMap[process.arch]}"`) console.log(`Starting main exe: ${mainExe()}`); exec(`"${mainExe()}" -UITEST ${PORT}`, (error, stdout, stderr) => { if (error) { From f6e25ae65468dd299a3e99b29492bb22ac2a4fde Mon Sep 17 00:00:00 2001 From: Steve Tynor Date: Thu, 11 Jun 2020 20:09:20 -0400 Subject: [PATCH 3/5] readme additions --- README.md | 6 +++++- uitest/README.md | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ae2ca73..f7e72ae 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,8 @@ To bundle the app for more environments, add an `environments` key to the bundle ] ``` -and repeat **step 3**. \ No newline at end of file +and repeat **step 3**. + +# Integration testing + +A simple Spectron based integration test is included in the `uitest` subdirectory. See its [README.md](uitest/README.md) for details. diff --git a/uitest/README.md b/uitest/README.md index eac1f3e..1dcabff 100644 --- a/uitest/README.md +++ b/uitest/README.md @@ -2,7 +2,7 @@ A sample NodeJS / Spectron-based end-to-end test for the Astilectron demo # Prerequisites -* Install npm (see [https://www.npmjs.com/get-npm]). +* Install npm (see (https://www.npmjs.com/get-npm)). # Step 1: install nodejs and the test dependencies @@ -20,4 +20,6 @@ Run the following command: * If you get an error suggesting that the test can't find the electron executable, ensure you run the demo at least once to allow astilectron to provision the electron artifacts -* "Uncaught javascript exception" ECONNRESET: \ No newline at end of file +* If the "before all" hook times out, the test is having trouble starting the chromedriver to connect to electron. (you may see an error message saying DevToolsActivePort does not exist). A common problem is a mis-match of the version of electron Astilectron is using vs. the version of the chromedriver bundled with spectron. See the version compatibility matrix at (https://github.com/electron-userland/spectron) and adjust the spectron version accordingly in package.json and rerun "npm install". + +* If all else fails, uncomment chromedriver and webdriver log settings test/hooks.js, rerun the test and then scour the logs. From 61c07015a66d080eff21916f39418a1636d1e41e Mon Sep 17 00:00:00 2001 From: Steve Tynor Date: Thu, 11 Jun 2020 20:23:22 -0400 Subject: [PATCH 4/5] remove vestige of boilerplate not used --- uitest/config.js | 9 --------- uitest/test/test.js | 1 - 2 files changed, 10 deletions(-) delete mode 100644 uitest/config.js diff --git a/uitest/config.js b/uitest/config.js deleted file mode 100644 index 9854a4f..0000000 --- a/uitest/config.js +++ /dev/null @@ -1,9 +0,0 @@ -const config = { - default: { - url: 'https://duckduckgo.com' - }, -}; - -exports.get = function get(env) { - return config[env] || config.default; -}; \ No newline at end of file diff --git a/uitest/test/test.js b/uitest/test/test.js index 24060f3..3f0b9fd 100644 --- a/uitest/test/test.js +++ b/uitest/test/test.js @@ -1,5 +1,4 @@ const hooks = require('./hooks'); -const config = require('../config').get(process.env.NODE_ENV); var app; From f8e30d2194c8a29626f30e73695fc9662f9dd1f5 Mon Sep 17 00:00:00 2001 From: Steve Tynor Date: Fri, 12 Jun 2020 08:59:10 -0400 Subject: [PATCH 5/5] generalize mac to support non-amd64 --- uitest/test/hooks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uitest/test/hooks.js b/uitest/test/hooks.js index d797e0e..4aa569e 100644 --- a/uitest/test/hooks.js +++ b/uitest/test/hooks.js @@ -28,7 +28,7 @@ if (archMap[process.arch] === undefined) { function mainExe() { if (process.platform === 'darwin') { - return `../output/darwin-amd64/${APPNAME}.app/Contents/MacOS/${APPNAME}`; + return `../output/darwin-${archMap[process.arch]}/${APPNAME}.app/Contents/MacOS/${APPNAME}`; } else if (process.platform === 'linux') { return `../output/linux-${archMap[process.arch]}/${APPNAME}`; } else if (process.platform === 'win32') { @@ -41,7 +41,7 @@ function mainExe() { function electronExe() { if (process.platform === 'darwin') { - return `../output/darwin-amd64/${APPNAME}.app/Contents/MacOS/vendor/electron-darwin-amd64/${APPNAME}.app/Contents/MacOS/${APPNAME}`; + return `../output/darwin-${archMap[process.arch]}/${APPNAME}.app/Contents/MacOS/vendor/electron-darwin-${archMap[process.arch]}/${APPNAME}.app/Contents/MacOS/${APPNAME}`; } else if (process.platform === 'linux') { return `../output/linux-${archMap[process.arch]}/vendor/electron-linux-${archMap[process.arch]}/electron`; } else if (process.platform === 'win32') { @@ -54,7 +54,7 @@ function electronExe() { function astilectronJS() { if (process.platform === 'darwin') { - return `../output/darwin-amd64/${APPNAME}.app/Contents/MacOS/vendor/astilectron/main.js`; + return `../output/darwin-${archMap[process.arch]}/${APPNAME}.app/Contents/MacOS/vendor/astilectron/main.js`; } else if (process.platform === 'linux') { return `../output/linux-${archMap[process.arch]}/vendor/vendor/astilectron/main.js`; } else if (process.platform === 'win32') {