I am trying to develop a project that runs on electron and capacitor android at the same time. I get this error when I run my electron app. It relates to the preload script. This script allows me to do IPC between the main thread and the vue thread. After installing capacitor none of my electron IPC works.
This is the error:
Uncaught Error: Cannot read properties of undefined (reading 'length')
at EventEmitter.<anonymous> (VM115 preloadx.js:49:57)
at EventEmitter.emit (VM14 node:events:390:28)
at Object.onMessage (VM113 renderer_init:69:746)
This is the renderer script. It is in the public folder. Node puts it into the 'out' folder, which is where it is found during execution.
const {
contextBridge,
ipcRenderer,
} = require("electron");
let validChannels = [
"api",
"ascii",
// some channels here ...
"py-neo",
];
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args)); // <-- error on this line!!
}
},
sendSync: (channel, data) => {
// whitelist channels
if (validChannels.includes(channel)) {
return ipcRenderer.sendSync(channel, data);
}
},
}
);
Here is some code from the main thread js file.
function createWindow() {
console.log("createWindow", __dirname);
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
//sandbox: true,
contextIsolation: true,
enableRemoteModule: true,
nodeIntegration: true,
nodeIntegrationInWorker: true,
preload: path.join(__dirname, "out", "preload.js"), // use a preload script
},
});
I am using electron 18.1.0 and node 16.15.1. I would like the IPC to work when I launch on the desktop. It does not need to work when I launch in Android studio. below is a segment from my package.json file.
"scripts": {
"start": "vue-cli-service build && electron --disable-gpu --disable-software-rasterizer . ",
"dist": "electron-builder",
"build": "vue-cli-service build",
"android": "npx cap sync android"
},
I did something like the following to remove the error message.
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => {
console.log(channel , ...args);
try {
func(...args);
}
catch {
console.log(...args);
}
//func(...args)
});
}
},
sendSync: (channel, data) => {
// whitelist channels
if (validChannels.includes(channel)) {
return ipcRenderer.sendSync(channel, data);
}
},
}
);
Related
I've been able to use the preload.js for sending message to API to get things done. I'm able to get responses just fine from iPC, but I'm not able to relay the responses from iPC back to the renderer and I don't understand what I'm missing.
index.js (main)
// Modules to control application life and create native browser window
const { app, BrowserWindow, remote, ipcMain } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
let mainWindow;
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready',() => {
createWindow();
require('./request.js')(mainWindow);
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
request.js - this is used in index/main thread above to process API requests, in this case requests to winax which I use with a separate 32 bit nodeJs interpreter because I couldn't get it to build for use with the same version of Node I can use for electron
const { ipcMain, ipcRenderer } = require('electron');
const { spawn } = require('child_process');
module.exports = function(mainWindow){
let winax;
ipcMain.on('winax', (event,arguments) => {
console.log('winax=');
console.log(arguments);
if(winax === undefined) {
console.log('spawning winax');
winax = spawn(
'C:\\Program Files\\nvm\\v14.20.0\\node.exe',
[ 'winax_microamp/index.js' ], {
shell: false,
stdio: ['inherit', 'inherit', 'inherit', 'ipc' ],
windowsHide: true
}
);
}
console.log('sending winax arguments');
/*
arguments = {
method: 'functionToRun',
owner: 'requestingProcess',
params: { required_params ... }
}
*/
winax.send(arguments);
winax.once('message', (message) => {
console.log('winax response=')
console.log(message);
mainWindow.webContents.postMessage('response', message);
});
});
}
preload.js
'use strict';
// preload.js
// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
// MAS: This function executes when the DOM is loaded so we should be able to add button interactions here
const { contextBridge, ipcRenderer } = require('electron');
let testAccDb = 'C:\\Users\\PZYVC7\\OneDrive - Ally Financial\\Access\\electron_microamp.accdb';
//const spawn = require('child_process').spawn
contextBridge.exposeInMainWorld(
'request', {
send: (func) => {
console.log('request.send.func=');
console.log(func);
if(func == 'openStagingTest'){
let args = {
method: 'requestStagingDatabase',
owner: 'ui',
params: {
path: testAccDb
}
};
ipcRenderer.send('winax',args);
} else if(func == 'closeStagingTest') {
let args = {
method: 'closeStagingDatabase',
owner: 'ui'
};
ipcRenderer.send('winax',args);
}
},
response: (message) => {
ipcRenderer.on('message', (message) => {
console.log('winax reply=');
console.log(message);
});
}
}
);
window.addEventListener('DOMContentLoaded', () => {
/*const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency]);
}*/
});
render.js
'use strict';
onLoad();
function onLoad(){
/*document.querySelector('.one').addEventListener('click',() => {
writeLog()
});*/
/*var testButton = document.querySelector('button[class=test]');
testButton.addEventListener('click', (e) => {
//C:\\Program Files\\nvm\\v14.20.0\\node.exe
window.main.asyncProc( '"C:\\Program Files\\nvm\\v14.20.0\\node.exe" winax_microamp/index.js' );
});*/
var buttons = document.querySelectorAll('button');
buttons.forEach((button) => {
//console.log('button=');
//console.log(button.className);
button.addEventListener('click', (event) => {
window.request.send(button.className);
});
});
}
Ugh... ok, so a few things I figured out eventually, I took hours to figure this out, and I'm not 100% sure if I'm understanding it properly so I appreciate any correction.
I think one issue that threw me off for longer than it should have is that the console.log for the preload.js goes into the developer tools rather than the system console like the main thread areas do.
Another thing throwing me off was that I was writing the implementation inside of preload.js, rather than in render.js. I confirmed it does work in both places.
If I want it in preload.js, I leave the implementation like this, and I confirmed it was firing here based on changing the log message a little
preload.js
response: (message) => {
ipcRenderer.on('response', (message) => {
console.log('expose reply=');
console.log(message);
});
}
}
If I want it in render instead, I need my preload to be more basic
response: (message) => {
ipcRenderer.on('response', message);
}
And then I need render to have this to process it there instead.
window.request.response((event,message) => {
console.log('winax reply=');
console.log(message);
});
Update
It seems like webpack is causing the issues.
If I replace the dist/background.js with:
console.log("background is running"); // Now visible ✅
const handler = (req, sender, sendResponse) => {
switch (req.type) {
case "message":
sendResponse({ data: "hi" });
break;
default:
break;
}
};
chrome.runtime.onMessage.addListener(handler);
Both the console log (in service worker) and response (in popup) are observed. Also, there are no errors.
Time to investigate further
Update #2
Upon further inspection, I noticed that the webpack output is wrapped in a function, but never called:
{
/***/ "./src/background.ts":
/*!***************************!*\
!*** ./src/background.ts ***!
\***************************/
/***/ (function () {
console.log("background is running");
const handler = (req, sender, sendResponse) => {
switch (req.type) {
case "message":
sendResponse({ data: "hi" });
break;
default:
break;
}
};
chrome.runtime.onMessage.addListener(handler);
/***/
}) // <==== adding () will make it an IIFE and everything works!
},
Question is how to automate this?
Update #3
Seems like the IIFE trick I mentioned above only works when there are no imports in background.js. As soon as I add any import, I get an error that the background script is not valid.
Adding module type property to background does not help:
// manifest.json
{
...
"background": {
"service_worker": "background.js",
"type": "module"
},
...
}
Update #4
Turns out this was caused by vendor splitting optimization in webpack:
// webpack.config.json
{
...
optimization: {
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
enforce: true,
chunks: "all",
},
},
},
},
...
}
Once I removed this, everything started working properly!
Would be nice to keep this around, but it is just an optimization after all, so if it breaks things, best to get rid of it.
How I figured this out? Well as I mentioned, everything worked a couple of commits ago. Back then I didn't have this optimization, so I tried removing it again, and everything started working again like magic.
Original Question
I had this working previously, so I am sure my setup is correct, but regardless of what I try, I now get
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
Also, I cannot find a solution online which indicates anything that differs from my setup.
Here is a MWE
dist folder structure:
dist/background.js
dist/index.html
dist/manifest.json
dist/popup.js
dist/runtime.js
dist/vendors.js
other misc files
// manifest.json
{
...
"background": {
"service_worker": "background.js"
},
...
}
// src/components/App.tsx
export default function App(): JSX.Element {
...
useEffect(() => {
chrome.runtime.sendMessage({ type: 'someMessage' }, ({ data }) => {
console.log(data);
});
}, []);
...
}
// src/background.ts
import { TSentResponse } from "./typings/background";
import { executeResponse } from "./utils/background";
console.log('sanity check'); // <=== does not fire 🤔
// also doesn't seem to be called 😥
const handleMessage = (req: { type: string }, sender: chrome.runtime.MessageSender, res: (response?: unknown) => void) => {
switch (req.type) {
case 'someMessage':
// an IIFE (worked fine before)
break;
default:
break;
}
return true; // due to asynchronous nature
};
chrome.runtime.onMessage.addListener(handleMessage);
My service worker is registered properly:
When I said above that I cannot see the logs for background, I mean when I check in the service worker dev tools (from above image), not the popup dev tool.
When I open the popup, I get the following errors:
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
Error handling response: TypeError: Cannot destructure property 'data' of 'undefined' as it is undefined.
I also don't see the service worker actually being registered - it did register before...
Is this a bug with MV3?
My repository (not fully up to date, but can be used to quickly check the above)
Follow these steps:
To send a message from the popup.js to the background service worker, first you need to get current tab id. to get the current tab id do as below:
popup.js
const messageKey = 'key-message-from-popup-to-background';
// Listen to get current tab info from the content_popup
document.addEventListener('_listener_getCurrentTabInfo', function (e) {
const tab = e.detail.response;
// Send a message to the background,js
chrome.tabs.sendMessage(tab.id, messageKey);
});
// Send message to content_popup to get current tab info
document.dispatchEvent(new CustomEvent('_dispatch_getCurrentTabInfo'));
content_popup.js
const config = {
scripts: [
// add ".js" files to web_accessible_resources in manifest.json
"popup/popup.js"
]
};
// Listen to get current tab info from the background.js
document.addEventListener('_dispatch_getCurrentTabInfo', function (e) {
// Key to help
const type = 'get_current_tab_info';
// Send a message to the background.js to get current tab info
chrome.runtime.sendMessage({type: type}, response => {
document.dispatchEvent(new CustomEvent('_listener_getCurrentTabInfo', {detail: {'response': response}}));
});
});
// prepare and add scripts
var scriptList = config['scripts'];
for (var i = 0; i < scriptList.length; i++) {
var s = document.createElement('script');
s.src = chrome.runtime.getURL(scriptList[i]);
s.onload = function () {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
}
background.js
// Get messages
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
// If request related to fetch data from API
if (request.url && request.url !== '') {
// Fetch http request and send back the response
fetch(request.url, request.init).then(function (response) {
return response.text().then(function (text) {
sendResponse([{
body: text,
status: response.status,
statusText: response.statusText,
}, null]);
});
}, function (error) {
sendResponse([null, error]);
});
// If request do not related to fetch data from API
} else {
detectMessageType(request, sender, sendResponse);
}
return true;
});
function detectMessageType(request, sender, sendResponse) {
// Check background request type
if (request && request.type) {
switch (request.type) {
case 'get_current_tab_info': {
getCurrentTabInfo(tab => {
// Send current tab info back to content_popup
sendResponse(tab);
});
break;
}
}
}
});
function getCurrentTabInfo(callback) {
chrome.tabs.query({
active: true,
currentWindow: true
}, function (tab) {
callback(tab[0]);
});
}
Get message in contentScript
chrome.runtime.onMessage.addListener((message, sender, response) => {
const message = message;
// Write your codes
});
This is the project structure (partial)
Project
-- popup
---- popup.js
---- content_popup.js
---- popup.html
---- popup.css
-- content_extension.js
-- background.js
-- manifest.js
Add the following code to the end of the body tag inside the popup.html file
<script src="content_popup.js"></script>
Update the manifest.json file like below
"manifest_version": 3,
.
.
.
"content_scripts": [
{
"js": [
"content_extension.js"
]
}
],
"action": {
"default_icon": ...,
"default_title": ...,
"default_popup": "popup/popup.html"
},
"background": {
"service_worker": "background.js"
},
"web_accessible_resources": [
{
"resources": [
"popup/popup.html",
"popup/popup.js"
]
}
],
...
I'm trying to setup communication between my Vue browser app and the electron main process, but it is not working.
Before startBot() is even called, I get an error message that __Dirname is unknown. But this constant is nowhere in to be found in the code.
What am I doing wrong?
https://gist.github.com/Quenos/7d6dbe4f5410739499ea9e3b0b4f961a.js
This is the background.js where I open the browser window with a preload. This has the purpose of making window available to the browser process
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1300,
height: 1100,
title: "Hedgehog TRDR Bot",
icon: path.join(__static, "hedgehog.jpg"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// __static is set by webpack and will point to the public directory
preload: path.resolve(__static, "preload.js"),
},
});
This is said preload.js
const { contextBridge, ipcRenderer } = require("electron");
const validChannels = ["READ_FILE", "WRITE_FILE"];
contextBridge.exposeInMainWorld("ipc", {
send: (channel, data) => {
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on: (channel, func) => {
if (validChannels.includes(channel)) {
// Strip event as it includes `sender` and is a security risk
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
The main process which contains listeners that then will do file handling
const { ipcMain } = require("electron");
const fs = require("fs");
var file;
ipcMain.on("OPEN_FILE", (event, payload) => {
console.log("daaro");
file = fs.createWriteStream(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
ipcMain.on("TEST_FILE", (event, payload) => {
console.log("daaro");
file.write(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
And the browser process which send requests to do file handling:
async startBot() {
window.ipc.send("OPEN_FILE", { path: "./HH_trdr_bot.csv" });
}
In the meantime I've found this article that perfectly answers my question
https://medium.com/swlh/how-to-safely-set-up-an-electron-app-with-vue-and-webpack-556fb491b83
Docs of Vue CLI Plugin Electron Builder contain description of how to do it, but it's a little scattered.
First, see how to configure a preload script, i.e.:
vue.config.js
module.exports = {
// ...
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
}
}
}
and then, repeat the path in BrowserWindow's webPreferences.preload constructor option.
preload: path.join(__dirname, 'preload.js')
Finally, in src/preload.js expose the IPC Renderer as described in IPC Without Node Integration.
src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
// whitelist channels
let validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['fromMain']
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
It might be also a good idea to unsubscribe the event listener when the component that subscribed is about to be destroyed.
I made mine working by putting the preload.js file inside public folder, in the https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#preload-files docs, there is a little diagram under Folder Structure section that says ├── public/ # Files placed here will be available through __static or process.env.BASE_URL, so what I did is simply, use the __static variable as described in docs, and append it with \preload.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: __static + '/preload.js', // <-- simple solution
}
})
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
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"