Skip to content
This repository has been archived by the owner on Feb 16, 2024. It is now read-only.

Spectron/Webdriver integration test example #62

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ To bundle the app for more environments, add an `environments` key to the bundle
]
```

and repeat **step 3**.
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.
34 changes: 33 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"flag"
"fmt"
"log"
"os/exec"
"strings"
"os"
"time"

Expand All @@ -29,19 +31,45 @@ var (
var (
fs = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
debug = fs.Bool("d", false, "enables the debug mode")
uitest = fs.Int("UITEST", 0, "if non-zero, the port that the uitest will use to attach to the main process's listener")
w *astilectron.Window
)

func main() {
// Create logger
l := log.New(log.Writer(), log.Prefix(), log.Flags())

// Parse flags
// Parse flags
fs.Parse(os.Args[1:])

/// 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{
Expand All @@ -51,6 +79,10 @@ func main() {
SingleInstance: true,
VersionAstilectron: VersionAstilectron,
VersionElectron: VersionElectron,

// for UITEST support:
TCPPort: &astiPort,
AcceptTCPTimeout: acceptTimeout,
},
Debug: *debug,
Logger: l,
Expand Down
3 changes: 3 additions & 0 deletions uitest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
package-lock.json
screenshots*png
25 changes: 25 additions & 0 deletions uitest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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

* 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.
18 changes: 18 additions & 0 deletions uitest/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
114 changes: 114 additions & 0 deletions uitest/test/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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 = {
"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') {
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') {
return `../output/windows-${archMap[process.arch]}/${APPNAME}.exe`;
} else {
console.log("FATAL: unhandled platform/os - add your variant here");
process.exit(1);
}
}

function electronExe() {
if (process.platform === 'darwin') {
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') {
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);
}
}

function astilectronJS() {
if (process.platform === 'darwin') {
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') {
return `${process.env.APPDATA}/${APPNAME}/vendor/astilectron/main.js`;
} else {
console.log("FATAL: unhandled platform - add your variant here");
process.exit(1);
}
}

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) {
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();
}
}
};
1 change: 1 addition & 0 deletions uitest/test/mocha.opts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--timeout 20000
29 changes: 29 additions & 0 deletions uitest/test/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const hooks = require('./hooks');

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('')
});

});