Can't launch selenium drivers while in Electron renderer process - javascript

Months ago I built an electron app that scrapes data from a web page using selenium and then visualizes it inside the electron webpage and everything worked fine.
After a few months of not using it, I'm having trouble because of many breaking changes inside electron and selenium themselves.
The major breaking change is that is not possible to start selenium webdrivers from the renderer process anymore, but I can start it only in the main process.
This below is a minimal non-working example of what I'm trying to do:
// index.js - entry point of the program
const electron = require("electron");
let app = electron.app
let mainWin;
app.on('ready', () => {
mainWin = new electron.BrowserWindow({
width: 100,
height: 100,
frame: true,
backgroundColor: '#222222',
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
});
mainWin.loadFile('./home.html');
mainWin.on('closed', function () {
console.log('byebye')
});
});
// home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
hello
</body>
<script type="text/javascript">require('./home.js')</script>
</html>
//home.js - script file for the home.html launched in the main process
const { Builder, By } = require('selenium-webdriver');
process.env.PATH += `${__dirname};`;
(async () => {
let driver = await new Builder().forBrowser('chrome').build();
await driver.get('https://www.google.com');
let test = await driver.findElements(By.css('div'))
console.log(test)
})()
The program gets completely stuck on the build for chrome webdrivers.
I am 100% sure that I am using the right chromedriver version and I never get an error or something useful, just empty, endless-running nothing.
Am I missing something (like webpreferences flags for the window) or this is a bug of electron/selenium?
It appears that this happens only when I'm using this on Linux.
Rebuilding the program to launch the drivers from the main process would mean rebuilding the program from scratch as it uses different windows and so on, and I can't pass the driver or anything else from the main process to the renderer using IRC since it breaks the driver object itself.

Ok, I managed to make it work on Linux too. The tweak is to use a preload script that will initialize the driver instance and then pass it to the renderer process by polluting the window object (it is the recommended way as shown here https://www.electronjs.org/docs/latest/tutorial/security#how-1). In this way, it is possible to obtain a fully working driver instance in the renderer process in Linux with selenium and electron.
Below are the changes to make it work:
// index.js - entry point of the program
const electron = require("electron");
let app = electron.app
let mainWin;
app.on('ready', () => {
mainWin = new electron.BrowserWindow({
width: 100,
height: 100,
frame: true,
backgroundColor: '#222222',
webPreferences: {
nodeIntegration: true,
contextIsolation: false
preload: path.join(__dirname,'preload.js')
},
});
mainWin.loadFile('./home.html');
mainWin.on('closed', function () {
console.log('byebye')
});
});
//preload.js
const { Builder, By } = require('selenium-webdriver');
(async () => {
let driver = await new Builder().forBrowser('chrome').build();
window.pollutingDriver = driver
})()
//home.js
//retrieve the driver in this way
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
var driver = null ;
(async () => {
while(!driver) {
driver = window.pollutingDriver
await sleep(500)
}
//do the main work on the page
})()

Related

How to organize Electron.js renderer code across multiple files without compromising security?

I'm building an Electron.js application, and I have been reading the documentation on context isolation, the main and renderer processes, and more.
I am struggling to understand how I can organize my renderer process code into multiple files or classes without using other frameworks such as Angular or Vue.
I have more or less achieved this goal by setting the sandbox option to false, which allows me to use require for external modules that are not part of Electron. However, as far as I understand, this is not a best practice because it poses security risks. Additionally, I encountered difficulties with classes when using this option. With only functions it works.
Here is the code that is giving me errors. How can I organize my renderer code into multiple files without going crazy?
main.js
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const nodePath = require("path");
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
}
ipcMain.handle('open-dialog', async (event, arg) => {
const result = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections']
});
return result;
});
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
const Test = require('./test');
contextBridge.exposeInMainWorld('myAPI', {
Test: Test
});
renderer.js
const api = window.myAPI;
const button = document.querySelector('#hi');
button.addEventListener('click', async () => {
const test = new api.Test();
test.hi();
});
test.js file where I put the functions or classes to be exported
class Test {
constructor() {
this.name = "TheSmith1222";
}
hi(){
console.log("Hi, " + this.name);
}
}
module.exports = {
Test
};
index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<button id="hi">Hi</button>
<script src="./renderer.js" type="module"></script>
</body>
</html>
Output button onclick:
renderer.js:5 Uncaught (in promise) TypeError: api.Test is not a constructor
at HTMLButtonElement.<anonymous> (renderer.js:5:15)

How do I wait to update an HTML header until promise is resolved?

I am building my first electron application and I am running into a JS error I am not sure how to fix. I have managed to setup the IPC for a basic password manager application. The use-case is simple:
I click an HTML button and the frontend connects to the backend and delivers a keyword from which to build a password.
Password is generated.
Upon completion, the password is returned to the frontend to be displayed via HTML in a header tag.
Here is an example of the expected behavior with the input string dog:
keyword --> dog
generated password --> dog-jdls4ksls
The issue I am seeing is that instead of printing the generated password, I am seeing:
[object Object]-jdls4ksls
My best guess is that, since I am using async/await, I am printing the promise memory object instead of the returned value. I do not know, however, how I would block to wait for completion. The code in question providing this output is the last line of render.js, which was called from the HTML body.
Any help would be appreciated!
For context, I am primarily a backend developer, with plenty of python and C/C++/C# experience. My goal is to rebuild a C#/.NET GUI application into an electron app.
Here is all my code.
main.js
const {app, BrowserWindow, ipcMain} = require("electron")
const path = require("path")
function generatePassword(keyword) {
console.log(keyword)
return keyword + '-' + Math.random().toString(36).substring(2,12)
}
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
resizable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('html/passwordGen.html')
}
app.whenReady().then(() => {
ipcMain.handle("generatePassword", generatePassword)
// console.log(generatePassword('test string')) // works
createWindow()
}).catch(error => {
console.log(error) // log error to console
app.quit() // quit the app
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('main', {
genPW: (keyword) => ipcRenderer.invoke("geåneratePassword", keyword)
})
render.js
async function testClick () {
const pw_root = document.getElementById("keyword")
const pw_label = document.querySelector("#password")
pw_label.innerText = await window.main.genPW(pw_root.value)
}
passwordGen.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Generator</title>
<link rel="stylesheet" href="../css/style.css">
<script src="../render.js"></script>
</head>
<body>
<h1>Password Generator</h1>
<input type="text" id="keyword" placeholder="Please enter a keyword...">
<button id="btn" onclick="testClick()">Generate Password</button>
<h1 id="password"></h1>
</body>
</html>
Edit:
Here is the code that worked. The accepted solution worked with the exception that I needed to either 1) keep the async/await on the generatePassword() or 2) convert it to .then() format as recommended in another solution.
main.js
const {app, BrowserWindow, ipcMain} = require("electron")
const path = require("path")
function generatePassword(keyword) {
console.log(keyword)
return keyword + '-' + Math.random().toString(36).substring(2,12)
}
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
resizable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('html/passwordGen.html')
}
app.whenReady().then(() => {
// ipcMain.handle("generatePassword", generatePassword)
// console.log(generatePassword('stink')) // works
ipcMain.handle('generatePassword', (_event, keyword) => {
console.log(keyword); // Testing
return generatePassword(keyword);
});
createWindow()
}).catch(error => {
console.log(error) // log error to console
app.quit() // quit the app
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('main', {
genPW: (keyword) => {
return ipcRenderer.invoke("generatePassword", keyword)
}
})
render.js
async function testClick () {
const pw_root = document.getElementById("keyword")
const pw_label = document.querySelector("#password")
pw_label.innerText = await window.main.genPW(pw_root.value)
// window.main.genPW(pw_root.value).then(res => {pw_label.innerText = res})
// ^^^ works as well if async/await removed
}
You were really close. Using Elctron's invoke method is the correct approach.
Within your main.js file, Electron's IPC handle signature contains the channel and listener arguments. In
your code you are calling your generatePassword() function in place of the listener argument. Instead, it should
be (event, ...args). In your specific case (event, keyword).
See ipcMain.handle(channel, listener)
for more information.
Additionally, within your preload.js script, all you need to do is add a return statement in front of your ipcRenderer.invoke method.
Finally, there is no need to use async on your testClick() function. Electron's invoke handles all of this.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
// Prevent garbage collection
let window;
function createWindow() {
window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
resizable: false,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
function generatePassword(keyword) {
console.log(keyword)
return keyword + '-' + Math.random().toString(36).substring(2,12)
}
electronIpcMain.handle('generatePassword', (event, keyword) => {
console.log(keyword); // Testing
return generatePassword(keyword);
});
preload.js (main process)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld(
'main', {
genPW: (keyword) => {
return ipcRenderer.invoke('generatePassword', keyword);
}
});
For sake of simplicity, I have included your render.js script within <script> tags below the closing </body> tag.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Emergency</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<div>
<label for="password">Password:</label>
<input type="text" id="password">
<input type="button" id="submit" value="Submit">
</div>
<div>
<label for="generated-password">Generated Password:</label>
<input type="text" id="generated-password" disabled>
</div>
</body>
<script>
document.getElementById('submit').addEventListener('click', () => {
window.main.genPW(document.getElementById('password').value)
.then((generatedPassword) => {
document.getElementById('generated-password').value = generatedPassword;
})
})
</script>
</html>
I dont think you can use async function as event listener, you need to use regular (not async) function here.
function testClick() {
const pw_root = document.getElementById("keyword")
const pw_label = document.querySelector("#password")
window.main.genPW(pw_root.value).then(res => {pw_label.innerText = res})
}
also, you have typo here: invoke("geåneratePassword")
Try using double await as in
await (await callToBackend)

How to securely preload content in the main process before creating the main window?

I'm building an App (Electron based) where I need to get an information from a third party website before the main window is created, but I'm a little bit confused about security measures. I'm using axios to do the HTTP request inside the main process because it is promise based and I can create the window after the website is fetched. My concerns are:
Enabling nodeIntegration is not good when messing with the renderer process because of cross-site-scripting attack. Should I include all nodejs modules in a preload.js like the following, for example.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Viewer</title>
</head>
<body>
<div id="box">
<form id='fo'>
<input type="text" id="num">
<button type="button" id="bttn">Random</button>
</form>
</div>
<script src="renderer.js"></script>
</body>
</html>
main.js
const electron = require('electron');
const cheerio = require('cheerio');
const axios = require('axios').default;
const path = require('path');
const {app, BrowserWindow, ipcMain, Menu, MenuItem,session} = electron;
let win;
let url = 'sampletext';
function createWindow() {
win = new BrowserWindow({
width: 400,
height: 250,
webPreferences:{
nodeIntegration: false,
contextIsolation: true,
preload: path.join(app.getAppPath(), 'preload.js')
},
show: false,
});
win.loadFile('index.html');
win.once('ready-to-show', () =>{
win.show();
});
win.on('closed', () =>{
win = null;
});
}
app.whenReady().then(getRequest().then(res => {
const $ = cheerio.load(res);
if($('infoNeeded')){
random = get_numbers($('infoNeeded').attr('href'));
}
createWindow();
}));
app.on('window-all-closed', () =>{
app.quit();
});
function getRequest() {
return axios.get(url).then(res => res.data).catch(err => console.log(err));
}
preload.js
//Instead of using getRequest() on main.js use this file
const electron = require('electron');
const remote = require('electron').remote;
const cheerio = require('cheerio');
const axios = require('axios').default;
let url = 'sampletext';
//So I can use it in renderer.js
window.getReq = function () {
return axios.get(url).then(res => res.data).catch(err => console.log(err));
}
window.parseInfo = function (data) {
const $ = cheerio.load(data);
if($('infoNeeded')){
return random = get_numbers($('infoNeeded').attr('href'));
}
return;
}
//Preload first request
window.getReq().then(doStuffHere);
renderer.js
let info;
//Keep updating the info
setInterval( () =>{
window.getReq().then(data => {
info = window.parseInfo(data);
});
}, 10000);
1) Is it ok to do nodejs require inside main process? If not, what's the secure way of doing it?
2) May I make HTTP requests inside main process? If yes, should I send a CSP header when doing so?
3) Instead of doing the request inside the main.js, should I use "webPreferences: preload" property and make the first HTTP request inside preload.js (Just like the above example) ? (I need to get the info before sending it to renderer.js)
I've already read https://www.electronjs.org/docs/tutorial/security, but I couldn't grasp their teaching. If you could provide an answer for how and when to use preload.js and CSP header I'll be very grateful.
Yes it is ok to use node.js require in the main process(use any library with error handling, cause it may crash the app)
You can make an HTTP request from the main process
You can use Preload.js if you need the code execution result in the renderer process.(You can also use the ipc)

Electron showOpenDialog arrow function (event.send) not working

I'm following the dialog example for opening files from: https://github.com/electron/electron-api-demos
I copied the code from the example. The open file dialog does in fact work and I'm able to select a file but can't figure out why the arrow function to send the file path back to renderer doesn't work(nothing is logged with console.log).
Can anyone spot what's wrong?
The project was started using electron-forge and my OS is linux.
Thanks
index.js
const { app, BrowserWindow, ipcMain, dialog, } = require('electron');
require('electron-reload')(__dirname);
const path = require('path');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit();
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, '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);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X 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();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
ipcMain.on('open-file-dialog', (event) => {
dialog.showOpenDialog(
{
properties: ['openFile',]
},
(files) => {
console.log('ok')
if (files) {
event.sender.send('select-file', files)
}
})
})
index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div>
<div>
<button class="demo-button" id="select-directory">Select file</button>
<span class="demo-response" id="selected-file"></span>
</div>
<br><br>
</div>
<script>
const electron = require('electron')
const { ipcRenderer } = electron
const selectDirBtn = document.getElementById('select-directory')
selectDirBtn.addEventListener('click', (event) => {
ipcRenderer.send('open-file-dialog')
})
ipcRenderer.on('select-file', (event, path) => {
console.log(path)
document.getElementById('selected-file').innerHTML = `You selected: ${path}`
})
</script>
</body>
</html>
The dialog API has been modified with the release of Electron 6.
dialog.showOpenDialog() and other dialog functions now return promises and no longer take callback functions. There also are synchronous counterparts which return the selection result in a blocking fashion, e.g. dialog.showOpenDialogSync().
Example usage (in renderer process)
const remote = require("electron").remote
const dialog = remote.dialog
dialog.showOpenDialog(remote.getCurrentWindow(), {
properties: ["openFile", "multiSelections"]
}).then(result => {
if (result.canceled === false) {
console.log("Selected file paths:")
console.log(result.filePaths)
}
}).catch(err => {
console.log(err)
})
As of February 2020, the electron-api-demos use Electron 5. That is why their dialog calling code still uses the old form.

How to automate ElectronJS app

We're looking to develop an ElectronJS app for particular website automation at our desk job, which includes common tasks like login, form filling, report downloading etc.
We've tried basic tutorial of ElectronJS, Spectron, NightmareJS, Puppeteer etc and all of them work fine separately, but very less documentation (although open github issues) are available on integration of each other.
We want to achieve following:
Login state (session) should not be deleted on ElectronJS app closing and should be available on restart of app.
Few menu buttons which initiates some automation tasks like download, form fill etc on existing browserWindow
We don't need headless automation, where some magic happens behind the scene. We need menu/button click based actions/tasks on current page only.
NightmareJS, Puppeteer etc all seems to start their own instances of web pages (since because they were built for testing of standalone apps) but what we need is automation of existing BrowserWindows.
Is puppeteer or nightmarejs correct tools for such goals? If yes, any documentation?
Or else, should we inject our own native JS events like mouseclick etc events in console to perform action?
You can use puppeteer-core. core version by default does not download Chromium, which you do not need if you want to control an Electron app.
In the test you then call launch method, where you define electron as the executable file instead of Chromium, like in following snippet:
const electron = require("electron");
const puppeteer = require("puppeteer-core");
const delay = ms =>
new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
(async () => {
try {
const app = await puppeteer.launch({
executablePath: electron,
args: ["."],
headless: false,
});
const pages = await app.pages();
const [page] = pages;
await page.setViewport({ width: 1200, height: 700 });
await delay(5000);
const image = await page.screenshot();
console.log(image);
await page.close();
await delay(2000);
await app.close();
} catch (error) {
console.error(error);
}
})();
Update for electron 5.x.y and up (currently up to 7.x.y, I did not test it on 8.x.y beta yet), where puppeteer.connect is used instead of launch method:
// const assert = require("assert");
const electron = require("electron");
const kill = require("tree-kill");
const puppeteer = require("puppeteer-core");
const { spawn } = require("child_process");
let pid;
const run = async () => {
const port = 9200; // Debugging port
const startTime = Date.now();
const timeout = 20000; // Timeout in miliseconds
let app;
// Start Electron with custom debugging port
pid = spawn(electron, [".", `--remote-debugging-port=${port}`], {
shell: true
}).pid;
// Wait for Puppeteer to connect
while (!app) {
try {
app = await puppeteer.connect({
browserURL: `http://localhost:${port}`,
defaultViewport: { width: 1000, height: 600 } // Optional I think
});
} catch (error) {
if (Date.now() > startTime + timeout) {
throw error;
}
}
}
// Do something, e.g.:
// const [page] = await app.pages();
// await page.waitForSelector("#someid")//
// const text = await page.$eval("#someid", element => element.innerText);
// assert(text === "Your expected text");
// await page.close();
};
run()
.then(() => {
// Do something
})
.catch(error => {
// Do something
kill(pid, () => {
process.exit(1);
});
});
Getting the pid and using kill is optional. For running the script on some CI platform it does not matter, but for local environment you would have to close the electron app manually after each failed try.
Simple demo repo:
https://github.com/peterdanis/electron-puppeteer-demo
Automation Script in Java using Selenium and ChromeDriver
package setUp;
import helper.Constants;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;
public class Test {
public static void main(String[] args) {
System.setProperty(Constants.WebDriverType, Constants.WebDriverPath + Constants.WindowsDriver);
ChromeOptions opt = new ChromeOptions();
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("chromeOptions", opt);
capabilities.setBrowserName("chrome");
capabilities.setVersion("73.0.3683.121");
ChromeOptions options = new ChromeOptions();
options.merge(capabilities);
options.setBinary("C:\\\\Program Files\\\\Audio\\\\Audio-Configuration\\\\Audio-Configuration.exe");
options.setCapability("chromeOptions", options);
ChromeDriver driver = new ChromeDriver(options);
try {
Thread.sleep(5000);
WebElement webElement = driver.findElement(By.xpath(
"/html/body/app-root/mat-drawer-container/mat-drawer/div/app-bottom-side-nav/div/app-settings-nav/div/div/a/div"));
webElement.click();
} catch (Exception e) {
System.out.println("Exception trace");
System.out.println(e);
}
}
}
Automation Script in JavaScript using Spectron (built on top-of ChromeDriver and WebDriverIO).
const Application = require("spectron").Application;
const path =
"C:/Program Files/Audio/Audio-Configuration/Audio-Configuration.exe";
const myApp = new Application({
path: path,
chromeDriverArgs: ["--disable-extensions"],
env: {
SPECTRON: true,
ELECTRON_ENABLE_LOGGING: true,
ELECTRON_ENABLE_STACK_DUMPING: true
}
});
const windowClick = async app => {
await app.start();
try {
// Identifying by class name
await app.client.click(".ic-setting");
// Identifying by Id
// await app.client.click("#left-btn");
} catch (error) {
// Log any failures
console.error("Test failed", error.message);
}
// Stop the application
await app.stop();
};
windowClick(myApp);
Spectron is the best match for electron build applications.
You will have access to all electron API.we can start and stop your app by spectron only.
We can run both packaged app or with out even packaging.
https://electronjs.org/spectron
You can use Spectron but if you want to look at documentation, Spectron is using webdriverio which has good documentation.
I recommend you to use Spectron because I tried to automate my tests with java-selenium but it fails some of case. If you want to use selenium, write below code to set capabilities to setup electron app to chromedriver.
ChromeOptions options = new ChromeOptions();
options.setBinary(binaryPath);
options.addArguments("--app=" + argPath);
options.setCapability("chromeOptions", options);
driver = new ChromeDriver(options);
Hope this will help to you.
If integrating with electron nightmare is a very good library to achieve this even it will be ready to distribute with it, here is the following useful documentation for the same resource1
and

Categories

Resources