Nightwatch not terminating after browser.end() - node.js

I'm running Nightwatch after launching a child process that starts up my local servers. Nightwatch runs the tests, they complete successfully, and the browser windows all close, but the nightwatch process continues to run after printing the message "OK. 10 total assertions passed.".
I thought it may have something to do with how I'm watching events on the nightwatch process, but as far as I can tell I am watching all events that would indicate Nightwatch is exiting.
The method shutdown() in runner.js is never called. How do I get Nightwatch to terminate when the tests finish?
Update
If I remove the last test in sign-in.js then Nightwatch exits as expected.
runner.js
import spawn from 'cross-spawn'
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing'
let servers
function shutdown (result) {
console.log('HERE', result)
try {
// Passing a negative PID to kill will terminate all child processes, not just the parent
if (servers) process.kill(-servers.pid)
} catch (e) {
console.error('Unable to shutdown servers, may need to be killed manually')
}
if (result) {
console.error(result)
process.exit(1)
} else {
process.exit(0)
}
}
function watch (child) {
child.on('close', shutdown)
child.on('disconnect', shutdown)
child.on('error', shutdown)
child.on('exit', shutdown)
child.on('uncaughtException', shutdown)
}
try {
servers = spawn('yarn', ['run', 'dev-all'], { cwd: '..', stdio: 'inherit', detached: true })
watch(servers)
// 2. run the nightwatch test suite against it
// to run in additional browsers:
// 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
// 2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2)
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'e2e/nightwatch.conf.js'])
}
if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome'])
}
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
watch(runner)
watch(process)
} catch (error) {
shutdown(error)
}
nightwatch.conf.js
require('babel-register')
var config = require('../../frontend/config')
// http://nightwatchjs.org/guide#settings-file
module.exports = {
src_folders: ['e2e/specs'],
output_folder: 'e2e/reports',
custom_assertions_path: ['e2e/custom-assertions'],
selenium: {
start_process: true,
server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-3.0.1.jar',
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': require('chromedriver').path
}
},
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
}
},
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true
}
},
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
}
}
}
}
sign-in.js (one of the tests)
import firebase from 'firebase-admin'
import uuid from 'uuid'
import * as firebaseSettings from '../../../backend/src/firebase-settings'
const PASSWORD = 'toomanysecrets'
function createUser (user) {
console.log('Creating user', user.uid)
let db = firebase.database()
return Promise.all([
firebase.auth().createUser({
uid: user.uid,
email: user.email,
emailVerified: true,
displayName: user.fullName,
password: PASSWORD
}),
db.ref('users').child(user.uid).set({
email: user.email,
fullName: user.fullName
}),
db.ref('roles').child(user.uid).set({
instructor: false
})
])
}
function destroyUser (user) {
if (!user) return
console.log('Removing user', user.uid)
let db = firebase.database()
try { db.ref('roles').child(user.uid).remove() } catch (e) {}
try { db.ref('users').child(user.uid).remove() } catch (e) {}
try { firebase.auth().deleteUser(user.uid) } catch (e) {}
}
module.exports = {
'Sign In links exist': browser => {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
const devServer = browser.globals.devServerURL
browser
.url(devServer)
.waitForElementVisible('#container', 5000)
browser.expect.element('.main-nav').to.be.present
browser.expect.element('.main-nav a[href^=\'https://oauth.ais.msu.edu/oauth/authorize\']').to.be.present
browser.expect.element('.main-nav a[href^=\'/email-sign-in\']').to.be.present
browser.end()
},
'Successful Sign In with Email shows dashboard': browser => {
const devServer = browser.globals.devServerURL
firebase.initializeApp(firebaseSettings.appConfig)
let userId = uuid.v4()
let user = {
uid: userId,
email: `${userId}#test.com`,
fullName: 'Test User'
}
createUser(user)
browser.url(devServer)
.waitForElementVisible('.main-nav a[href^=\'/email-sign-in\']', 5000)
.click('.main-nav a[href^=\'/email-sign-in\']')
.waitForElementVisible('button', 5000)
.setValue('input[type=text]', user.email)
.setValue('input[type=password]', PASSWORD)
.click('button')
.waitForElementVisible('.main-nav a[href^=\'/sign-out\']', 5000)
.end(() => {
destroyUser(user)
})
}
}
After the tests complete successfully, I see the following:
grimlock:backend egillespie$ ps -ef | grep nightwatch
501 13087 13085 0 1:51AM ttys000 0:02.18 node ./node_modules/.bin/nightwatch --presets es2015,stage-0 --config e2e/nightwatch.conf.js --env chrome

I was not explicitly closing the Firebase connection. This caused the last test to hang indefinitely.
Here's how I am closing the connection after doing test cleanup:
browser.end(() => {
destroyUser(user).then(() => {
firebase.app().delete()
})
})
The destroyUser function now looks like this:
function destroyUser (user) {
if (!user) return Promise.resolve()
let db = firebase.database()
return Promise.all([
db.ref('roles').child(user.uid).remove(),
db.ref('users').child(user.uid).remove(),
firebase.auth().deleteUser(user.uid)
])
}

In my case (nightwatch with vue/vuetify) after each test like so:
afterEach:function(browser,done){
done();
}

AfterAll(async()=>{
await closeSession();
await stopWebDriver();
}
place this in the config file #Erik Gillespie

Nightwatch still have this issue with the browser.end()
If you run Nightwatch with node.js you can stop the process
by doing something like that:
browser.end(() => {
process.exit();
});
It will close the browser and end the process.

I have tried the following method:
In "nightwatch.conf.js",
"test_settings" {
"default" {
"silent": true,
...
},
...
}
I set "silent" from true to false.
It lead to becoming verbose in the console. And the chromedriver.exe will exit peacefully after running the tests
I was using the vue template from: https://github.com/vuejs-templates/pwa
My platform:
Windows 7 (64bit)
node v8.1.3
"nightwatch": "^0.9.16",
"selenium-server": "^3.6.0",
"chromedriver": "^2.33.1"

Related

import runs all code from module instead of just the imported function

This is my index.js file, located in the ./src directory:
import { MongoClient } from "mongodb";
import CharacterDAO from "./dao/character";
import GearDAO from "./dao/gear";
import { startServer } from "./server";
import { seedData } from "./dataSeed";
// connect mongoDb, seed data if needed, run fastify server
export const runServer = async ({ dbUrl, dbName, environment, port }) => {
// test seed data when starting server if running a test suite
if (environment === "test") {
await seedData({
hostUrl: dbUrl,
databaseName: dbName
});
}
await MongoClient.connect(dbUrl, {
poolSize: 50,
useNewUrlParser: true,
useUnifiedTopology: true,
wtimeout: 2500
})
.then(async conn => {
const database = await conn.db(dbName);
// inject database connection into DAO objects
CharacterDAO.injectDB(database);
GearDAO.injectDB(database);
// start the fastify server
startServer(port);
})
.catch(err => {
console.log(err.stack);
// process.exit(1);
});
};
const serverArguments = process.argv.slice(2).map(arg => {
return arg.split("=")[1];
});
const serverOptions = {
dbUrl: serverArguments[0],
dbName: serverArguments[1],
environment: serverArguments[2],
port: serverArguments[3]
};
runServer({
...serverOptions
});
jestconfig.json
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}
Test script from package.json used to run the test (db credentials are omitted)
"test": "dbUrl=mongodb+srv://sdaw-dsawdad-dsadsawd#cluster0-jopi5.mongodb.net dbName=untitled-combat-game-test environment=test port=4000 jest --config jestconfig.json"
My test file:
import { runServer } from "../index";
beforeAll(async () => {
const serverOptions = {
dbUrl: process.env.dbUrl,
dbName: process.env.dbName,
environment: process.env.environment,
port: process.env.port
};
console.log(serverOptions);
await runServer({
...serverOptions
});
});
describe("mock test", () => {
it("should run a basic test", () => {
expect(true).toBe(true);
});
});
What happens when I run the test:
the test script runs runServer
the index.js file runs runServer
This causes a invalid URI error (since the process.argv referenced in index.js does not include a valid mongodb URI). I double-checked this by commenting out the runServer call at the bottom of my index.js file - and everything runs fine.
Moving the runServer function to a different file and importing it from there also solves the issue. So importing in both index.js and the test file does not result in multiple calls.
What am I doing wrong?
Importing/requiring a file evaluates the code inside of it (read: runs the code inside of it). You're not technically doing anything wrong, but for the purpose of your tests the code as you have written it won't work.
In your index.js file you are executing runServer(). Whenever that file is imported/required, that function call is also run.
Having a start.js file or similar which will actually start your server is a common pattern. This will help you avoid the issue you're experiencing.
I would split the definition of your server and invoking your server into two different files, say server.js and index.js. I will leave the fixing up of the imports to you, but this is the idea:
server.js
// connect mongoDb, seed data if needed, run fastify server
export const runServer = async ({ dbUrl, dbName, environment, port }) => {
// test seed data when starting server if running a test suite
if (environment === "test") {
await seedData({
hostUrl: dbUrl,
databaseName: dbName
});
}
await MongoClient.connect(dbUrl, {
poolSize: 50,
useNewUrlParser: true,
useUnifiedTopology: true,
wtimeout: 2500
})
.then(async conn => {
const database = await conn.db(dbName);
// inject database connection into DAO objects
CharacterDAO.injectDB(database);
GearDAO.injectDB(database);
// start the fastify server
startServer(port);
})
.catch(err => {
console.log(err.stack);
// process.exit(1);
});
};
index.js
import { runServer } from './server';
const serverArguments = process.argv.slice(2).map(arg => {
return arg.split("=")[1];
});
const serverOptions = {
dbUrl: serverArguments[0],
dbName: serverArguments[1],
environment: serverArguments[2],
port: serverArguments[3]
};
runServer({
...serverOptions
});

How to create electron auto updates using private github repository?

The update is being detected but I'm unable to download it to my app.
I get the following error:
Status: Update Available
Status: Error in auto-updater. Error: Cannot download "https://api.github.com/repos/[username]/[repo-name]/releases/assets/15151663", status 404: Not Found.
The problem appears only using private github repository not public!!
I've tried installing auto updates to a clean electron react-boilerplate and it works perfectly fine with private github repository.. So i'm a bit at a loss what to do here..
I did some research and it seems like app-update.yml should contain github token (electron-builder should generate it) but my app-update.yml (which is located in release/win-unpacked/resources) does not contain a token...
It only contains this info:
owner: [username]
repo: [repo-name]
provider: github
updaterCacheDirName: [appname]
How can I generate it?
Other comment states that I should have a separate release-only repository which I do, but it still doesn't work.
Electron Autoupdater with Private GitHub Repository?
Other people say that downgrading versions fix this problem, but I also saw people say that doesn't fix it and downgrading isn't really a good option.
My steps of adding gh-token:
I setup my github info in package.json (this token is being ignored)
"publish": {
"provider": "github",
"owner": "[username]",
"repo": "[repo-name]",
"token": "[gh-token]",
"private": true
}
"repository": {
"type": "git",
"url": "https://github.com/[username]/[repo-name].git"
},
So I add it to my main.js aswell just in case.
autoUpdater.setFeedURL({
provider: 'github',
repo: '[repo-name]',
owner: '[username]',
private: true,
token: '[gh-token]'
});
process.env.GH_TOKEN = "[gh-token]";
When I remove setFeedURL I get the exact same error as in this questions:
https://github.com/electron-userland/electron-builder/issues/2641
latest.yml (generated file in github releases along side installer.exe installer.exe.blockmap and installer.msi)
version: [version-number]
files:
- url: [app-name.exe]
sha512: [string]
size: [file-size]
path: [app-name.exe]
sha512: [string]
releaseDate: [release-date]
versions i'm using:
"electron": "^3.0.10",
"electron-builder": "^20.38.4",
"electron-updater": "^4.0.0",
full main.js
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
export default class AppUpdater {
constructor() {
log.transports.file.level = 'info';
autoUpdater.logger = log;
autoUpdater.checkForUpdatesAndNotify();
}
}
let mainWindow = null;
autoUpdater.setFeedURL({
provider: 'github',
repo: '[repo-name]',
owner: '[username]',
private: true,
token: '[gh-token]'
});
process.env.GH_TOKEN = "[gh-token]";
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
require('electron-debug')();
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
return Promise.all(
extensions.map(name => installer.default(installer[name], forceDownload))
).catch(console.log);
};
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
const sendStatusToWindow = (text) => {
log.info(text);
if(mainWindow){
mainWindow.webContents.send('message', text);
}
}
autoUpdater.on('checking-for-update', () => {
sendStatusToWindow('Checking for update...');
});
autoUpdater.on('update-available', (info) => {
sendStatusToWindow('Update available.');
dialog.showMessageBox({
message: 'update available !!'
});
});
autoUpdater.on('update-not-available', (info) => {
sendStatusToWindow('Update not available.');
});
autoUpdater.on('error', (err) => {
sendStatusToWindow('Error in auto-updater. ' + err);
});
autoUpdater.on('download-progress', (progressObj) => {
let log_message = "Download speed: " + progressObj.bytesPerSecond;
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%';
log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')';
sendStatusToWindow(log_message);
})
autoUpdater.on('update-downloaded', (info) => {
sendStatusToWindow('Update downloaded');
dialog.showMessageBox({
message: 'Update downloaded, restarting app..'
});
autoUpdater.quitAndInstall();
});
app.on('ready', async () => {
autoUpdater.checkForUpdatesAndNotify();
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
await installExtensions();
}
mainWindow = new BrowserWindow({
show: false,
width: 1024,
height: 728
});
mainWindow.webContents.openDevTools();
mainWindow.loadURL(`file://${__dirname}/app.html`);
mainWindow.webContents.on('did-finish-load', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
mainWindow.show();
mainWindow.focus();
}
});
mainWindow.on('closed', () => {
mainWindow = null;
});
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
new AppUpdater();
});
I package my app using:
"package": "yarn build && electron-builder build --publish never"
And then I publish it to github releases using:
"gh-publish": "electron-builder --x64 -p always"
import {autoUpdater} from 'electron-updater';
autoUpdater.setFeedURL({
provider: 'github',
owner: 'your_username',
repo: 'your_repo_name',
private: true,
token: process.env.GH_TOKEN, // provide your github access token, with repo:access
});
For the reference to pulling the update, look into this repo:
https://github.com/iffy/electron-updater-example

How to load fastify-env environments synchronously?

I'm working on fastify microservice and would like to use the fastify-env library to validate my env inputs and provide defaults throughout the whole app.
const fastify = require('fastify')()
fastify.register(require('fastify-env'), {
schema: {
type: 'object',
properties: {
PORT: { type: 'string', default: 3000 }
}
}
})
console.log(fastify.config) // undefined
const start = async opts => {
try {
console.log('config', fastify.config) // config undefined
await fastify.listen(3000, '::')
console.log('after', fastify.config) // after { PORT: '3000' }
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
How can I use the fastify.config object before the server starts?
Use ready() https://www.fastify.io/docs/latest/Server/#ready to wait for all plugins to be loaded. Then call listen() with your config variable.
try {
await fastify.ready(); // will load all plugins
await fastify.listen(...);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.register loads plugins asynchronously AFAIK. If you'd like to immediately use things from a specific plugin use:
fastify
.register(plugin)
.after(() => {
// This particular plugin is ready!
});

NodeJS VM2 proper way to access console when set to 'redirect'

I'm using the VM2 package to run user code. I'm trying to intercept console output and have set the NodeVM object's console property to 'redirect':
// Create a new sandbox VM for this request
const vm = new NodeVM( {
console: 'redirect',
timeout: 30000,
sandbox: { request, state, response },
require: {
external: true
}
});
According to the documentation that redirects console output to 'events'. I'm new to NodeJS, how do I hook into those events to capture the console.log messages executed inside the Sandbox?
After digging through the source code, I found this file where the event emit is occuring:
sandbox.js
if (vm.options.console === 'inherit') {
global.console = Contextify.readonly(host.console);
} else if (vm.options.console === 'redirect') {
global.console = {
log(...args) {
vm.emit('console.log', ...Decontextify.arguments(args));
return null;
},
info(...args) {
vm.emit('console.info', ...Decontextify.arguments(args));
return null;
},
warn(...args) {
vm.emit('console.warn', ...Decontextify.arguments(args));
return null;
},
error(...args) {
vm.emit('console.error', ...Decontextify.arguments(args));
return null;
},
dir(...args) {
vm.emit('console.dir', ...Decontextify.arguments(args));
return null;
},
time: () => {},
timeEnd: () => {},
trace(...args) {
vm.emit('console.trace', ...Decontextify.arguments(args));
return null;
}
};
}
All you need to do to listen to these events is to bind an event listener on the vm you've created:
// Create a new sandbox VM for this request
const vm = new NodeVM( {
console: 'redirect',
require: {
external: ['request']
}
});
vm.on('console.log', (data) => {
console.log(`VM stdout: ${data}`);
});
Likewise, you can bind to console.log, console.info, console.warn, console.error, console.dir, and console.trace. Hopefully this will save someone else some time.

How to add `--health-cmd='mysqladmin ping --silent'` and `--health-interval='1s'` options to dockerode's createContainer() function?

I want to use node.js to manipulate docker containers. And I found a module called dockerode, which seems like the tool I need.
However, I haven't found any way to add --health-cmd and --health-interval options to createContainer function.
These 2 options are used in docker run command. You can find them by docker run --help | grep health:
--health-cmd string Command to run to check health
--health-interval duration Time between running the check (ms|s|m|h) (default 0s)
I've tried the following code, but the 2 options don't work inside the code.
const Docker = require('dockerode')
const docker = new Docker({socketPath: '/var/run/docker.sock'})
docker.createContainer({
Image: 'mysql:5.7',
name: 'myContainer',
env: ['MYSQL_ROOT_PASSWORD=mypassword'],
'health-cmd': 'mysqladmin ping --silent',
'health-interval': '1s',
HostConfig: {
PortBindings: {
'3306/tcp': [
{
HostPort: '3306'
}
]
}
}
}, (err, container) => {
if (err) {
console.error(`create err:${err}`)
} else {
container.start((err, data) => {
if (err) {
console.error(`start err: ${err}`)
} else {
console.log(`data: ${data}`)
}
})
}
})
Thanks to the help of this post. I have found the document of Docker Engine API and figured out the correct way to add the 2 options:
const Docker = require('dockerode')
const docker = new Docker({socketPath: '/var/run/docker.sock'})
docker.createContainer({
Image: 'mysql:5.7',
name: 'myContainer',
env: ['MYSQL_ROOT_PASSWORD=mypassword'],
Healthcheck: {
Test: ['CMD-SHELL', 'mysqladmin ping'],
Interval: 1000000000
},
HostConfig: {
PortBindings: {
'3306/tcp': [
{
HostPort: '3306'
}
]
}
}
}, (err, container) => {
if (err) {
console.error(`create err:${err}`)
} else {
container.start((err, data) => {
if (err) {
console.error(`start err: ${err}`)
} else {
console.log(`data: ${data}`)
}
})
}
})
You can find that createContainer calls POST /containers/create from the source code located in node_modules/dockerode/lib/docker.js.

Resources