I am working on an Angular 9 + Electron app that reads files names (not the content) from the File System and presents them on a panel in a tree-like structure. I have the Angular portion working just fine on the browser, but when I try to run it within the Electron context, the panel does not show the tree-structure. The only way to make it show it, is to resize the Electron-app window or force an event (click on a button). The question that I have is how do I make it render the tree-structure once the data arrives?
The data coming from the Electron process shows up on the console when I add a console.log for the Electron service within Angular and Angular gets it and processes the data. But the console stops printing when the data has to be transferred from the app.component.ts file to its child component. The console stop with the following warning:
"Security Warning: webFrame.executeJavaScript was called without worldSafeExecuteJavaScript enabled. This is considered unsafe. worldSafeExecuteJavaScript will be enabled by default in Electron 12."
And stops printing anything else.
Here is the code that I am using:
In index.html
...
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
...
In app.ts (Electron main file):
...
function createWindow() {
win = new BrowserWindow(
{
"width": 800, "height": 600,
"title": "My Life Notes...",
"icon": path.join(__dirname,`./favicon.ico`),
webPreferences: {
"nodeIntegration": true,
"worldSafeExecuteJavaScript": true
}
}
);
win.loadURL(
url.format({
"pathname": path.join(__dirname, `./index.html`),
"protocol": "file:",
"slashes": true
})
);
win.webContents.openDevTools();
win.on("closed", () => {
win = null;
});
}
...
In app.component.ts:
...
NOTES_DATA: NotesNodeImp[]=[];
constructor(private electron_service: ElectronServiceFile,
private select_service: SelectedNodeService){
this.electron_service.loadFiles();
}
...
ngOnInit(){
this.electron_service.notes.subscribe( notes =>{
console.log("app.component notes ", notes);
notes.forEach( note => {
let name_parts = note.split('-');
let notesObj =new NotesNodeImp(Number.parseInt(name_parts[0]));
notesObj.name = note;
notesObj.label = name_parts[1];
this.NOTES_DATA.push(notesObj);
});
this.NOTES_DATA = Object.assign([], this.NOTES_DATA);
})
...
In app.component.html
<div class="app-nav">
<app-navigation [data_saved]="NOTES_DATA"></app-navigation>
</div>
In electron.service.ts:
...
constructor() {
if ((<any>window).require) {
try {
this.ipc = (<any>window).require('electron').ipcRenderer;
} catch (e) {
throw e;
}
} else {
console.warn('App not running inside Electron!');
}
this.ipc.on('getNotesResponse', (event, notes) => {
this.notes.next(notes);
console.log("available notes ", notes);
});
}
loadFiles(){
console.log('load files: ');
this.ipc.send('loadNotes');
}
...
In navigation.component.ts (child)
...
data: NotesNodeImp[]=[];
#Input('data_saved')
set data_saved (d: NotesNodeImp[]){
console.log("data1 " , d)
this.data = d;
}
...
Here is the console:
The console
The image shows that the loadFiles method gets called first, then app.component.ts sends an empty array to its child (navigation.component.ts). However, when the data does arrived from the Electron ipcMain process, app.component.ts receives it, processes it and it doesn't seem like it sends it to its child component.
The problem is that the browser engine is not sending a change event when the data arrives to the component. Angular has several ways to deal with this. I chose
this.zone.run(() => this.NOTES_DATA );
Which to send the data to the components that need to know. For an explanation on how to use zones: https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html
Related
The Challenge
I am working on an Electron/Angular app that will display an emergency message to multiple computers in the event of a tornado or other incident. To accomplish this, I am using a WebSocket protocol (Signral R) on the backend to send the alert, which then triggers Electron's main.js (running in the background) to launch a window on any devices running the app.
The arrival of the message triggers the window to launch, but I also need to use the message in the Angular part of the app (which does not initialize until after the window is open) to indicate the nature of the emergency. To avoid timing problems, I planned to import a function into my app.component.ts that would be called at ngOnInit to retrieve an emergency variable from main.js.
The Problem
Angular does not like the import from raw JavaScript. Intuitively you would think Angular would only import the exported class. However it appears to try and import Electron as well (generating a conflict with 'fs' and 'path' that is explained here), generating the following error message:
./node_modules/electron/index.js:1:11-24 - Error: Module not found: Error: Can't resolve 'fs' in 'C:\Users\me\electron-app\node_modules\electron'
./node_modules/electron/index.js:3:13-28 - Error: Module not found: Error: Can't resolve 'path' in 'C:\Users\me\electron-app\node_modules\electron'
BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.
If you want to include a polyfill, you need to:
- add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
- install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
resolve.fallback: { "path": false }
In spite of doing considerable research, I have not found anything approaching a solution. Any ideas for only importing the information I want would be much appreciated. Here are my files:
main.js (simplified somewhat)
const signalR = require("#microsoft/signalr")
const { app, shell, BrowserWindow, Menu, nativeImage, Tray } = require('electron')
const path = require("path");
const url = require("url");
//various setup logic goes here
//...
webSocketConnection.on("EmergencyActivation", (jsonData) => {
createElectronWindow();
const emergencyMessage = jsonData
});
module.exports = class Emergency {
getEmergency() {
return emergencyMessage
}
}
app.component.ts
import { Component, OnInit } from '#angular/core';
import { Emergency } from './../../main';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(private emergency: Emergency) {}
ngOnInit() {
console.log(this.emergency.getEmergency())
}
}
Though I do not use Angular, I think the issue is communication between your main process and your render process (or
lack
thereof).
Don't try to communicate between processes by importing from the main process to the render process (which can't
be done). Instead, use Inter-Process Communication.
IE: Once an emergency message has been received via webSocket, create the window and then send an IPC message to that newly
created window.
I have needed to "mock" your webSocketConnection functionality for the sake of the below example.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const nodePath = require('path');
let window;
function createWindow() {
window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
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', () => {
listenForEmergency();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
// Mock of "webSocketConnection.on" function (for testing only)
function listenForEmergency() {
let message = 'Emergency: We have no beer'
console.log('Wait 5 seconds before "receiving" emergency message'); // Testing
setTimeout(() => { showEmergencyWindow(message); }, 5000);
}
// Create window and send message via IPC
function showEmergencyWindow(message) {
window = createWindow();
window.webContents.send('emergency', message); // <-- Use of IPC messaging
}
// Create window and send message via IPC
// webSocketConnection.on("EmergencyActivation", (jsonData) => {
// window = createWindow();
// window.webContents.send('emergency', jsonData); // <-- Use of IPC messaging
// });
preload.js (main process)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld(
'electronAPI', {
emergency: (message) => {
ipcRenderer.on('emergency', message)
}
});
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>
<h1>Emergency</h1>
<div id="message"></div>
</body>
<script>
window.electronAPI.emergency((event, message) => {
document.getElementById('message').innerText = message;
})
</script>
</html>
Long story short I am working on a single page application that sends commands over a local network. Testing out Electron JS and I can't even seem to get a simple button to work. I feel like I am not linking the logic between main.js and index.js somehow but for the life of me I cannot figure out the correct way to do it. I have even put breakpoints in index.js and through main.js & index.html but none of the breakpoints are hit aside from the ones in main.js. I put a simple function in a preload.js file and that function is correctly called but the one I am trying to attach to a button located in index.html and index.js is never even being hit. A lot of the commented out code is things I want to remember or things I have noticed a different method of creating and just wanted to try and see if that worked. If anyone has any answers or guidance it would be greatly appreciated! :D
Below is my main.js
//#region ---for dev only | hot reload
try {
require('electron-reloader')(module)
} catch (_) {}
//#endregion
const electron = require('electron');
const {app, BrowserWindow, Menu} = require('electron');
const path = require('path');
const ipcMain = electron.ipcMain;
//#region globals
const SRC_DIR = '/src/'
const IMG_DIR = '/assets/images'
//#endregion
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
//frame: false,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
//Used to auto open dev tools for debugging
//win.openDevTools();
win.loadFile('src/index.html');
// win.loadURL(url.format({
// pathname: path.join(__dirname, 'index.html'),
// protocol: 'file',
// slashes: true
// }));
}
app.whenReady().then(() => {
//nativeTheme.shouldUseDarkColors = true;
createWindow();
})
//closes app processes when window is closed
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
})
var menu = Menu.buildFromTemplate([
{
label: 'Menu',
submenu: [
{label: 'Edit'},
{type: 'separator'},
{
label: 'Exit',
click() {
app.quit();
}
}
]
}
])
Menu.setApplicationMenu(menu);
Here is index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Ecas Software</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<p id="myText">Let's get started :)</p>
<button id="myBtn">Change Text</button>
<script type="text/javascript" src="./index.js" ></script>
</body>
</html>
Lastly here is my index.js (aka my first and only renderer?)
const electron = require('electron');
const chgBtn = document.getElementById('myBtn');
function replaceText(selector, text){
const element = document.getElementById(selector);
if (element) element.innerText = text;
}
chgBtn.onclick = function() {
replaceText('myText', 'no boom...');
}
// chgBtn.addEventListener('click', function(){
// // if (document.getElementById('myText').innerText == 'boom'){
// // replaceText('myText','no boom...');
// // } else {
// // replaceText('myText','boom');
// // }
// document.alert("working function");
// });
//chgBtn.addEventListener('click', replaceText('myText','no boom...'));
Why you have this error
The problem here is that you didn't use your scripts files the way Electron was intended.
If you use the Devtools Console (by uncommenting win.openDevTools()), you should see this error in your console :
Uncaught ReferenceError: require is not defined (from index.js file)
This is because your index.js file is loaded as a "normal javascript file". If you want to use the Node syntaxe (aka the "require" syntaxe), you need to do it in your preload script. Only the preload script can use the require syntaxe, since it is the only script allowed by Electron to use Node.
You can also use other javascripts files, by import it in your HTML as you did for the index.js file, but you should remove the require call. As the "require" call (on the first line) will throw and error, all the following code will not run. This is why your button did not react on click.
The correct way to do it
If you need to use some methods from the Electron Renderer API (such as the ipcRenderer), you need to put it in your preload script.
If you want to use your own script, in a separate file, you can also do it, you will not be able to directly call Electron API. There is a solution if you want to call the Electron API in your own script, it is called the Context Bridge. This allows you to create an object in your preload script, that can use the Electron API. You can give this object a name, and then call it from your others script by using the window global object.
For example, if you want to use ipcRenderer.send(channel, payload) :
// Preload script
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('theNameYouWant',
{
send: (channel, payload) => ipcRenderer.send(channel, payload)
}
)
// index.js file, imported in your HTML file
window.theNameYouWant.send("channel-name", { someData: "Hello" })
In your example
// Add this in your main.js file to see when a user click on the button from main process
ipcMain.on("button-clicked", (event, data) => console.log(data))
// Preload script
const { contextBridge, ipcRenderer } = require("electron")
contextBridge.exposeInMainWorld("electron", {
send: (channel, payload) => ipcRenderer.send(channel, payload),
})
// index.js
const chgBtn = document.getElementById("myBtn")
function replaceText(selector, text) {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
chgBtn.onclick = function () {
replaceText("myText", "no boom...")
window.electron.send("button-clicked", { someData: "Hello" })
}
After putting off testing for a while now due to Cypress not allowing visiting chrome:// urls, I decided to finally understand how to unit/integration test my extension - TabMerger. This comes after the many times that I had to manually test the ever growing functionality and in some cases forgot to check a thing or two. Having automated testing will certainly speed up the process and help me be more at peace when adding new functionality.
To do this, I chose Jest since my extension was made with React (CRA). I also used React Testing Library (#testing-library/react) to render all React components for testing.
As I recently made TabMerger open source, the full testing script can be found here
Here is the test case that I want to focus on for this question:
import React from "react";
import { render, fireEvent } from "#testing-library/react";
import * as TabFunc from "../src/Tab/Tab_functions";
import Tab from "../src/Tab/Tab";
var init_groups = {
"group-0": {
color: "#d6ffe0",
created: "11/12/2020 # 22:13:24",
tabs: [
{
title:
"Stack Overflow - Where Developers Learn, Share, & Build Careersaaaaaaaaaaaaaaaaaaaaaa",
url: "https://stackoverflow.com/",
},
{
title: "lichess.org • Free Online Chess",
url: "https://lichess.org/",
},
{
title: "Chess.com - Play Chess Online - Free Games",
url: "https://www.chess.com/",
},
],
title: "Chess",
},
"group-1": {
color: "#c7eeff",
created: "11/12/2020 # 22:15:11",
tabs: [
{
title: "Twitch",
url: "https://www.twitch.tv/",
},
{
title: "reddit: the front page of the internet",
url: "https://www.reddit.com/",
},
],
title: "Social",
},
};
describe("removeTab", () => {
it("correctly adjusts groups and counts when a tab is removed", () => {
var tabs = init_groups["group-0"].tabs;
const { container } = render(<Tab init_tabs={tabs} />);
expect(container.getElementsByClassName("draggable").length).toEqual(3);
var removeTabSpy = jest.spyOn(TabFunc, "removeTab");
fireEvent.click(container.querySelector(".close-tab"));
expect(removeTabSpy).toHaveBeenCalledTimes(1);
expect(container.getElementsByClassName("draggable").length).toEqual(2); // fails (does not remove the tab for some reason)
});
});
I mocked the Chrome API according to my needs, but feel that something is missing. To mock the Chrome API I followed this post (along with many others, even for other test runners like Jasmine): testing chrome.storage.local.set with jest.
Even though the Chrome storage API is mocked, I think the issue lies in this function which gets called upon initial render. That is, I think the chrome.storage.local.get is not actually being executed, but am not sure why.
// ./src/Tab/Tab_functions.js
/**
* Sets the initial tabs based on Chrome's local storage upon initial render.
* If Chrome's local storage is empty, this is set to an empty array.
* #param {function} setTabs For re-rendering the group's tabs
* #param {string} id Used to get the correct group tabs
*/
export function setInitTabs(setTabs, id) {
chrome.storage.local.get("groups", (local) => {
var groups = local.groups;
setTabs((groups && groups[id] && groups[id].tabs) || []);
});
}
The reason I think the mocked Chrome storage API is not working properly is because when I manually set it in my tests, the number of tabs does not increase from 0. Which forced me to pass a prop (props.init_tabs) to my Tab component for testing purposes (https://github.com/lbragile/TabMerger/blob/f78a2694786d11e8270454521f92e679d182b577/src/Tab/Tab.js#L33-L35) - something I want to avoid if possible via setting local storage.
Can someone point me in the right direction? I would like to avoid using libraries like jest-chrome since they abstract too much and make it harder for me to understand what is going on in my tests.
I think I have a solution for this now, so I will share with others.
I made proper mocks for my chrome storage API to use localStorage:
// __mocks__/chromeMock.js
...
storage: {
local: {
...,
get: function (key, cb) {
const item = JSON.parse(localStorage.getItem(key));
cb({ [key]: item });
},
...,
set: function (obj, cb) {
const key = Object.keys(obj)[0];
localStorage.setItem(key, JSON.stringify(obj[key]));
cb();
},
},
...
},
...
Also, to simulate the tab settings on initial render, I have a beforeEach hook which sets my localStorage using the above mock:
// __tests__/Tab.spec.js
var init_ls_entry, init_tabs, mockSet;
beforeEach(() => {
chrome.storage.local.set({ groups: init_groups }, () => {});
init_ls_entry = JSON.parse(localStorage.getItem("groups"));
init_tabs = init_ls_entry["group-0"].tabs;
mockSet = jest.fn(); // mock for setState hooks
});
AND most importantly, when I render(<Tab/>), I noticed that I wasn't supplying the id prop which caused nothing to render (in terms of tabs from localStorage), so now I have this:
// __tests__/Tab.spec.js
describe("removeTab", () => {
it("correctly adjusts storage when a tab is removed", async () => {
const { container } = render(
<Tab id="group-0" setTabTotal={mockSet} setGroups={mockSet} />
);
var removeTabSpy = jest.spyOn(TabFunc, "removeTab");
var chromeSetSpy = jest.spyOn(chrome.storage.local, "set");
fireEvent.click(container.querySelector(".close-tab"));
await waitFor(() => {
expect(chromeSetSpy).toHaveBeenCalled();
});
chrome.storage.local.get("groups", (local) => {
expect(init_tabs.length).toEqual(3);
expect(local.groups["group-0"].tabs.length).toEqual(2);
expect(removeTabSpy).toHaveBeenCalledTimes(1);
});
expect.assertions(4);
});
});
Which passes!!
Now on to drag and drop testing 😊
I'm in the process of learning to use Electron, and while trying to have my application communicate with the front end I am aware I need to use the ipcRenderer to gain a reference to the DOM elements and then pass that information to ipcMain.
I tried to follow much of the advice suggested here and here, but both of these examples use require('electron').ipcMain and whenever I try to include my script that will be interacting with the front-end into my HTML, nothing occurs since Uncaught ReferenceError: require is not defined. I've been searching for a few hours and haven't had any luck finding a solution - so clearly I'm doing something wrong.
My main.js is very simple, I just create my window and then I create an ipc listener as so:
const { app, BrowserWindow } = require("electron");
const ipc = require('electron').ipcMain;
function createWindow() {
const window = new BrowserWindow({
transparent: true,
frame: false,
resizable: false,
center: true,
width: 410,
height: 550,
});
window.loadFile("index.html");
}
app.whenReady().then(createWindow);
ipc.on('invokeAction', (event, data) => {
var result = "test result!";
event.sender.send('actionReply', result);
})
Within the file that I wish to manipulate the DOM with, I attempt to get the element ID and then add an event listener as seen here:
const ipc = require('electron').ipcRenderer;
const helper = require("./api");
var authenticate_button = ipcRenderer.getElementById("authenticate-button");
var authButton = document.getElementById("authenticate-button");
authButton.addEventListener("click", () => {
ipc.once('actionReply', (event, response) => {
console.log("Hello world!");
})
ipc.send('invokeAction');
});
function onAuthenticateClick() {
helper.authenticateLogin(api_public, api_secret, access_public, access_secret);
}
and finally, my HTML only consists of a button that I wish to attach my event listener to:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project Test</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="main-container">
<button id="authenticate-button" type="submit" onclick="">Authenticate</button>
<p id="status-label">Not Authenticated</p>
</div>
<script src="script.js"></script>
</body>
</html>
If anyone could help point me in the right direction as to how to get this basic functionality to work, it would be very helpful!
As mentioned by AlekseyHoffman, the reason you can't access ipcRenderer in your frontend js file is because you have nodeIntegration set to false. That said, there's a reason it's set to false by default now; it makes your app far less secure.
Let me suggest an alternate approach: rather than trying to access ipcRenderer directly from your frontend js by setting nodeIntegration to true, access it from preload.js. In preload.js, you can selectively expose ipcMain functions (from your main.js file) you want to access on the frontend (including those that can send data back from main.js), and call them via ipcRenderer there. In your frontend js, you can access the preload.js object that exposes those functions; preload.js will then call those main.js functions via ipcRenderer and return the data back to the frontend js that called it.
Here's a simple, but fully working example (these files should be sufficient to build an electron app with two-way communication between main.js and frontend. In this example, all of the following files are in the same directory.):
main.js
// boilerplate code for electron..
const {
app,
BrowserWindow,
ipcMain,
contextBridge
} = require("electron");
const path = require("path");
let win;
/**
* make the electron window, and make preload.js accessible to the js
* running inside it (this will allow you to communicate with main.js
* from the frontend).
*/
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false,
preload: path.join(__dirname, "./preload.js") // path to your preload.js file
}
});
// Load app
win.loadFile(path.join(__dirname, "index.html"));
}
app.on("ready", createWindow);
// end boilerplate code... now on to your stuff
/**
* FUNCTION YOU WANT ACCESS TO ON THE FRONTEND
*/
ipcMain.handle('myfunc', async (event, arg) => {
return new Promise(function(resolve, reject) {
// do stuff
if (true) {
resolve("this worked!");
} else {
reject("this didn't work!");
}
});
});
Note, I'm using an example of ipcMain.handle because it allows two-way communication and returns a Promise object - i.e., when you access this function from the frontend via preload.js, you can get that Promise back with the data inside it.
preload.js:
// boilerplate code for electron...
const {
contextBridge,
ipcRenderer
} = require("electron");
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
// end boilerplate code, on to your stuff..
/**
* HERE YOU WILL EXPOSE YOUR 'myfunc' FROM main.js
* TO THE FRONTEND.
* (remember in main.js, you're putting preload.js
* in the electron window? your frontend js will be able
* to access this stuff as a result.
*/
contextBridge.exposeInMainWorld(
"api", {
invoke: (channel, data) => {
let validChannels = ["myfunc"]; // list of ipcMain.handle channels you want access in frontend to
if (validChannels.includes(channel)) {
// ipcRenderer.invoke accesses ipcMain.handle channels like 'myfunc'
// make sure to include this return statement or you won't get your Promise back
return ipcRenderer.invoke(channel, data);
}
},
}
);
renderer process (i.e. your frontend js file - I'll call it frontend.js):
// call your main.js function here
console.log("I'm going to call main.js's 'myfunc'");
window.api.invoke('myfunc', [1,2,3])
.then(function(res) {
console.log(res); // will print "This worked!" to the browser console
})
.catch(function(err) {
console.error(err); // will print "This didn't work!" to the browser console.
});
index.html
<!DOCTYPE html>
<html>
<head>
<title>My Electron App</title>
</head>
<body>
<h1>Hello Beautiful World</h1>
<script src="frontend.js"></script> <!-- load your frontend script -->
</body>
</html>
package.json
{
"name": "myapp",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
The files above should be sufficient to have a fully working electron app with communication between main.js and the frontend js. Put them all in one directory with the names main.js, preload.js, frontend.js, and index.html, and package.json and launch your electron app using npm start. Note that in this example I am storing all the files in the same directory; make sure to change these paths to wherever they are stored on your system.
See these links for more info and examples:
Electron documentation on inter-process communication
An overview of why IPC is needed and the security issues of setting nodeintegration to true
The require is not defined because you didn't enable nodeIntegration on the window. Set it to true in your window config:
const window = new BrowserWindow({
transparent: true,
frame: false,
resizable: false,
center: true,
width: 410,
height: 550,
webPreferences: {
nodeIntegration: true
}
})
We are using a javascript framework(Not react) to render the ui.
main.js
function logout(){
someObj.lock($('#container'), 'Logging out', true);
document.location = app.context + `/${appName}/signout.action?name=${appName}`;
}
function action(event){
switch(event.target){
case 'user:logout':
logout();
break;
case 'user:application':
document.location = app.context + "/";
break;
}
}
module.exports = {
action: action,
logout: logout
}
main.js along with another js file renders a navbar and a dropdown. My intention is to check whether title, dropdown in the navbar is rendered. Also I
am testing whether the browser redirect takes place in the right way.
action method takes an event object and based on its type, either performs signout('user:logout') or redirects to application page('user:application').
tests/main.js
import main from '../main';
describe("some title", () => {
it("some behavior", () => {
let event = {
target: 'user:logout'
}
let app = {
context: ''
}
let appName = 'Some app';
main.logout = jest.fn();
someObj = jest.fn();
someObj.lock = jest.fn();
document.location.assign = jest.fn();
main.action(event);
expect(main.logout).toHaveBeenCalled();
expect(document.location.assign).toBeCalledWith(app.context + `/${appName}/signout.action?name=${appName}`);
})
});
In the test file, I am trying to mock logout function. However it is executing someObj.lock function. someObj is not availabe to tests/main.js
and I am mocking it as well. I'm not sure whether I have to use spyOn instead. I'm using document.location.assign to test for browser redirects.
This test is not working and the terminal is displaying TypeError: Could not parse "/application name/signout.action?name=application name" as URL.
I have spent an entire day testing this feature but to no avail. I need some advice on the best way to test this feature.
Links explored: Intercept navigation change with jest.js (or how to override and restore location.href)
jest documentation