I am trying to follow tutorial on how to display WebViewer for pdf. However I get this error
Uncaught SyntaxError: redeclaration of const docViewerindex.html:23:14note: Previously declared at line 20, column 12index.html:20:12
I can see that there is wrong redeclaration however I I am new to this and I have no idea how to fix it. Can someone please advise, I am completely new to this
Based on this
I had a look on this page https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Redeclared_parameter and. tried to change const to let but that still doesn't help
<!DOCTYPE html>
<html>
<head>
<title>Basic WebViewer</title>
</head>
<!-- Import WebViewer as a script tag -->
<script src='WebViewer/lib/webviewer.min.js'></script>
<body>
<div id='viewer' style='width: 1024px; height: 600px; margin: 0 auto;'></div>
<script>
WebViewer({
path: 'WebViewer/lib', // path to the PDFTron 'lib' folder on your server
licenseKey: 'Insert commercial license key here after purchase',
initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf',
// initialDoc: '/path/to/my/file.pdf', // You can also use documents on your server
}, document.getElementById('viewer'))
.then(instance => {
const docViewer = instance.docViewer;
const annotManager = instance.annotManager;
docViewer.on('documentLoaded', () => {});
});
</script>
</body>
</html>
try:
.then(instance => {
const annotManager = instance.annotManager;
instance.docViewer.on('documentLoaded', () => {});
});
the is what solved it
.then(instance => {const { docViewer, annotManager } = instance;
docViewer.on('documentLoaded', () => {});
Related
I am trying to send data from a main window to another window in electronJS.
How my app works is there is a main window with many selections. On clicking each selection, a new window will open, and the window will show data that is related to that selection. For now, what works is that clicking each selection will open a new window, but I am unable to pass data over to the new window.
I have read through the electron docs but most seem to be focused on data from renderer to main. The example which shows data passing from main to renderer didn't help me and I still struggle to implement what I want.
I tried looking for some help here
Trying to send data from one electron window to another via ipc
Electron: How to securely inject global variable into BrowserWindow / BrowserView?
Electron: How to pass the message/data from preload to renderer?
and tried to implement the suggestions but I still can't get it to work.
I have 2 html files (index.html and details.html), a main.js, a preload.js and a renderer.js for the details.html
Here are my codes:
main.js
// main.js
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
height: 1000,
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.whenReady().then(() => {
createWindow()
ipcMain.on('open-selection-window', (event) => {
openNewWindow()
})
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()
})
// 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 require them here.
const openNewWindow = () => {
const Window = new BrowserWindow({
width: 1000,
height: 1000,
title: ' details',
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
Window.loadFile('details.html')
}
preload.js (note the contextbridge portion)
// preload.js
const axios = require('axios');
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.
// this function is called when the user clicks on a selection, it will get the details
const getselectionDetail = (argument) => {
axios.get(`http://apiurl/${argument}`)
.then(response => {
return response.data;
})
}
// this function is called when user press search button, it will search for the selections thru API call,
// and then display the results,
// set onclick function for each result,
const searchselections = (text) => {
//use axios to make a get request to the url
axios.get(`http://apiurl/${text}`)
.then(response => {
const selections = response.data;
// for each element in selections, append a div with the class of search-result and append the html
selections.forEach(selection => {
document.getElementById('results').innerHTML += `
<div class="search-result">
<p>${selection.name}</p>
</div>`;
});
// for each search result, need to set it such that on click, the contextbridge will send the selection details to the renderer
// and then the renderer will load the selection details
const searchResults = document.getElementsByClassName('search-result');
for (let i = 0; i < searchResults.length; i++) {
searchResults[i].onclick = () => {
contextBridge.exposeInMainWorld(
'selection',
// this is to get a new window to open and sends data to the main process
ipcRenderer.send('open-selection-window', getselectionDetail(selections[i].name))
);
// send data to the renderer -> this doesn't work?
contextBridge.exposeInMainWorld(
'details',
getselectionDetail(selections[i].name)
)
}
}
})
.catch(error => {
console.log(error);
}
)
}
renderer.js
const detail_name = document.getElementById('detail-name');
// load the data from window, need to append to html
console.log(window.details) <-- this doesn't work
index.html
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <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>Search function</title>
</head>
<body>
<h1>Search function</h1>
<div class="root">
<!-- text input for the search -->
<input type="text" id="search-input" placeholder="Search">
<!-- search button -->
<button id="search-button" class="search-button" type="button">
Search
</button>
</div>
<!-- div to display the search results -->
<div id="results"></div>
</body>
</html>
details.html
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <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>Details</title>
</head>
<body>
<h1>Details</h1>
<div class="result">
<!-- div for name -->
<div class="name">
<h5>Name:</h5>
<p id="detail-name"></p>
</div>
</div>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
I appreciate any tips/guidance!
You can use BroadcastChannel like below
const qr = new BroadcastChannel("test");
qr.postMessage(JSON.stringify({ var: "val" }));
and listen from another file like
const bc = new BroadcastChannel("test");
bc.onmessage = (event) => {
this.data = JSON.parse(event.data);
};
You can read more about this on mdn.broadcastchannel
The comments can't hold so much text so I'll post the code here to show what I've tried, but doesn't work.
I added the top code in the onclick assignment
preload.js
// for each search result, need to set it such that on click, the contextbridge will send the selection details to the renderer
// and then the renderer will load the selection details
const searchResults = document.getElementsByClassName('search-result');
for (let i = 0; i < searchResults.length; i++) {
searchResults[i].onclick = () => {
contextBridge.exposeInMainWorld(
'selection',
// this is to get a new window to open and sends data to the main process
ipcRenderer.send('open-selection-window', getselectionDetail(selections[i].name))
);
const qr = new BroadcastChannel("test");
qr.postMessage(JSON.stringify({ var: "val" }));
}
}
renderer.js
// set windows onload event
window.addEventListener('load', () => {
const bc = new BroadcastChannel("test");
bc.onmessage = (event) => {
this.data = JSON.parse(event.data);
console.log(JSON.parse(event.data));
};
});
I think you are trying to do too much in your preload.js script. Additionally, I don't think you can
use contextBridge.exposeInMainWorld more than once per window.
Place your Axios calls in your main process. This will dramatically simplify your preload.js script.
Additionally, just use your preload.js script to transfer data between processed. That way, it will simplify it even
further.
Use your html files only to display the UI, make it interactive and dynamically render content. The remainder can be
handled in the main process.
Note: I have mocked your Axios calls with simplified fake data for testing.
Apart from your main.js file doing the usual things, listen via IPC calls from:
index.html to initiate a search (via invoke) and return the results (via handle).
index.html to open details.html window and send "details" to new window.
If your Axios calls returns promises, see the bottom of the preload.js script for IPC promise use.
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 searchWindow;
let detailsWindow;
function createSearchWindow() {
const searchWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 1000,
height: 1000,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
searchWindow.loadFile('index.html')
.then(() => { searchWindow.show(); });
return searchWindow;
}
function createDetailsWindow() {
const detailsWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 300,
height: 300,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
detailsWindow.loadFile('details.html')
.then(() => { detailsWindow.show(); });
return detailsWindow;
}
electronApp.on('ready', () => {
searchWindow = createSearchWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createSearchWindow();
}
});
// ---
electronIpcMain.handle('search', (event, data) => {
// Use "data" variable for Axios call.
return [
'Result 1',
'Result 2',
'Result 3',
'Result 4',
'Result 5'
]
})
electronIpcMain.on('openDetailsWindow', (event, data) => {
// Use "data" for Axios call.
let results = {
'Result 1': 'Details for Result 1',
'Result 2': 'Details for Result 2',
'Result 3': 'Details for Result 3',
'Result 4': 'Details for Result 4',
'Result 5': 'Details for Result 5'
}
detailsWindow = createDetailsWindow();
detailsWindow.webContents.send('pushDetails', results[data]);
})
I have used a white listed channel name configuration for your preload.js script. This simplifies things even further
and prevent your preload.js script from being used for more than just simple channel name calling and data transfer.
If you are after an alternative design for your preload.js script, just let me know.
preload.js (main process)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'openDetailsWindow'
],
// From main to render.
'receive': [
'pushDetails'
],
// From render to main and back again.
'sendReceive': [
'search'
]
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
/**
* Render --> Main
* ---------------
* Render: window.ipcRender.send('channel', data); // Data is optional.
* Main: electronIpcMain.on('channel', (event, data) => { methodName(data); })
*
* Main --> Render
* ---------------
* Main: windowName.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRender.receive('channel', (data) => { methodName(data); });
*
* Render --> Main (Value) --> Render
* ----------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', (event, data) => { return someMethod(data); });
*
* Render --> Main (Promise) --> Render
* ------------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', async (event, data) => {
* return await promiseName(data)
* .then(() => { return result; })
* });
*/
In your index.html (search) file, place an event listener on the "search" button (and the results div). Once clicked,
send an IPC message to the main thread to retrieve your Axios data call. Once retrieved, dynamically add the results to
the DOM.
On a click in your results div, use event delegation to find the result that was clicked and again, use IPC to
send a message to open up the "details" window.
Note: For simplicity, I have added your required renderer.js code in-between <script> tags.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Search function</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<h1>Search function</h1>
<div class="root">
<input type="text" id="search-input" placeholder="Search">
<input type="button" id="search-button" class="search-button" value="Search">
</div>
<div id="results"></div>
</body>
<script>
let results = document.getElementById('results');
document.getElementById('search-button').addEventListener('click', () => {
window.ipcRender.invoke('search', document.getElementById('search-input').value)
.then((data) => {
let output = '';
for (let item of data) {
output += `<div class="search-results">${item}</div>`;
}
results.innerHTML = output;
})
})
results.addEventListener('click', (event) => {
window.ipcRender.send('openDetailsWindow', event.target.innerText);
})
</script>
</html>
The details.html file fill automatically receive the detailed data from main process upon opening.
details.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Details</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<h1>Details</h1>
<div class="result">
<div class="name">
<h5>Name:</h5>
<p id="detail-name"></p>
</div>
</div>
</body>
<script>
window.ipcRender.receive('pushDetails', (details) => {
document.getElementById('detail-name').innerText = details;
})
</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" })
}
I have loaded the jitsi meet script in the body of my public.html, and i have a component as follows:
<template>
<div class="container">
<div id="meet"></div>
</div>
</template>
<script>
export default {
name: "ServiceMeet",
mounted() {
const domain = "meet.jit.si";
const options = {
roomName: "PickAnAppropriateMeetingNameHere",
width: 700,
height: 700,
parentNode: document.querySelector("#meet"),
};
const api = new JitsiMeetExternalAPI(domain, options);
console.log(api.getVideoQuality());
},
};
</script>
When I try to run I get an error saying 18:21 error 'JitsiMeetExternalAPI' is not defined no-undef, however in the background i can see that the meet is working fine, so I do I fix the error or dismiss it.
You could disable the linting error, but I would recommend specifying it as a global variable instead.
.eslintrc.js
module.exports = {
globals: {
JitsiMeetExternalAPI: true
}
}
It should work if you prefix the global with window:
const api = new window.JitsiMeetExternalAPI(domain, options);
I'm using vue.js to create a webpage, and I'm trying to use a class (Authenticator) that is outside of my two files (Login.html and Login.js) that are using vue.js. But I can't seem to manage to import that class (Authenticator) in my Login.js file to use it's function...
Is there a way to do that? Here is my code:
Login.html
<!DOCTYPE html>
<html>
<head>
</head>
<body class="background">
<div class="border">
<div id="app">
[...]
</div> <!-- End of app -->
</div> <!-- End of border -->
<script src="./vue.js"></script>
<script src="./Login.js"></script>
<script src="../controller/Authenticator.js"></script>
</body>
</html>
Login.js
var app = new Vue({
el: '#app',
data: {
username: null,
password: null,
activeUsers: []
},
methods: {
verifyLogin: function () {
// Verify if fields have been filled
if (null === this.username || null === this.password) {
window.alert("Error, please fill out the form correctly!");
} else {
// If fields are filled, verify the authentication
var authenticator = new Authenticator(this.username);
var authenticated = authenticator.authenticateUser(this.username, this.password);
[...]
} else if (null === authenticated) {
// Error, if authentication failed
window.alert("Wrong password, please try again!")
}
}
}
}
});
Authenticator.js
const MongoClient = require('mongodb').MongoClient;
var url = "mongodb://192.168.99.100:27017/UserDB";
const bcrypt = require('bcrypt'); // Hash algorithm
const saltRounds = 10;
export default class Authenticator {
constructor(iNewUserName) {
this.mActiveUserNames.push(iNewUserName);
}
async authenticateUser(iUserName, iPassword) {
[...]
}
}
I get the error "Uncaught SyntaxError: Unexpected token 'export'" on the webpage console. Also, I tried to change the method of exporting by doing:
module.exports = Authenticator;
But then I get the error "Uncaught ReferenceError: require is not defined at Authenticator.js:2" from the webpage console.
Can someone help me please, I looked everywhere online, and I cannot find the solution?
Thank you.
I am unable to use JSDOM (version 13.0.0) to load scripts from the local filesystem with a relative path.
I have taken a look at the following questions but they do not answer my question:
jsdom can't load local html and javascript (I have already followed the runScripts and resources suggestion there).
File foo.js:
var jsdom = require('jsdom')
var html = `<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<script src="bar.js"></script>
</head>
<body>
<div>Test</div>
</body>
</html>`
global.window = new jsdom.JSDOM(html, { runScripts: "dangerously", resources: "usable" }).window
console.log('foo')
File bar.js:
console.log('bar')
Here is the error I get:
$ node foo.js
foo
Error: Could not load script: "bar.js"
at onErrorWrapped (/Users/lone/so/node_modules/jsdom/lib/jsdom/browser/resources/per-document-resource-loader.js:41:19)
at Object.check (/Users/lone/so/node_modules/jsdom/lib/jsdom/browser/resources/resource-queue.js:72:23)
at request.then.catch.err (/Users/lone/so/node_modules/jsdom/lib/jsdom/browser/resources/resource-queue.js:124:14)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:746:11)
at startup (internal/bootstrap/node.js:240:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:564:3) Error: Tried to fetch invalid URL bar.js
at ResourceLoader.fetch (/Users/lone/so/node_modules/jsdom/lib/jsdom/browser/resources/resource-loader.js:84:29)
at PerDocumentResourceLoader.fetch (/Users/lone/so/node_modules/jsdom/lib/jsdom/browser/resources/per-document-resource-loader.js:16:42)
at HTMLScriptElementImpl._fetchExternalScript (/Users/lone/so/node_modules/jsdom/lib/jsdom/living/nodes/HTMLScriptElement-impl.js:92:30)
at HTMLScriptElementImpl._eval (/Users/lone/so/node_modules/jsdom/lib/jsdom/living/nodes/HTMLScriptElement-impl.js:161:12)
at HTMLScriptElementImpl._poppedOffStackOfOpenElements (/Users/lone/so/node_modules/jsdom/lib/jsdom/living/nodes/HTMLScriptElement-impl.js:126:10)
at OpenElementStack.pop (/Users/lone/so/node_modules/jsdom/lib/jsdom/browser/htmltodom.js:17:12)
at Object.endTagInText [as END_TAG_TOKEN] (/Users/lone/so/node_modules/parse5/lib/parser/index.js:2153:20)
at Parser._processToken (/Users/lone/so/node_modules/parse5/lib/parser/index.js:657:55)
at Parser._processInputToken (/Users/lone/so/node_modules/parse5/lib/parser/index.js:684:18)
at Parser._runParsingLoop (/Users/lone/so/node_modules/parse5/lib/parser/index.js:440:18)
How can I load a local JavaScript file while using JSDOM?
JSDOM doesn't know where to look for that file locally while executing. So running your example you can follow any of this two approaches.
1st Approach
You have to wait for the script file to load and execute.
Create a three files index.html,index.js and test.js into the same folder.
index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
abc
<script src='index.js'></script>
</body>
</html>
index.js
document.body.textContent = 123;
test.js
'use strict';
const { JSDOM } = require('jsdom');
const options = {
resources: 'usable',
runScripts: 'dangerously',
};
JSDOM.fromFile('index.html', options).then((dom) => {
console.log(dom.window.document.body.textContent.trim());
setTimeout(() => {
console.log(dom.window.document.body.textContent.trim());
}, 5000);
});
// console output
// abc
// 123
2nd Approach
Set the external scripts base root folder in JSDOM env.
js/index.js
console.log('load from jsdom');
var loadFromJSDOM = 'load from jsdom';
test.js
'use strict';
const { JSDOM } = require('jsdom');
JSDOM.env({
html: "<html><body></body></html>",
documentRoot: __dirname + '/js',
scripts: [
'index.js'
]
}, function (err, window) {
console.log(window.loadFromJSDOM);
}
);
Read more from these references
https://github.com/jsdom/jsdom/issues/1867
jsdom.env: local jquery script doesn't work
Great answer from front_end_dev. It helped me a lot and I will share how my code works with this solution to be more clear. Maybe will help others.
import "#testing-library/jest-dom";
import { logDOM } from "#testing-library/dom";
import { JSDOM } from "jsdom";
import fs from "fs";
import path from "path";
const html = fs.readFileSync(path.resolve(__dirname, "../index.html"), "utf8");
let dom;
let container;
jest.dontMock("fs");
function waitForDom() {
return new Promise((resolve) => {
dom = new JSDOM(html, {
runScripts: "dangerously",
resources: "usable",
url: `file://${path.resolve(__dirname, "..")}/index.html`,
});
dom.window.document.addEventListener("DOMContentLoaded", () => {
resolve();
});
});
}
beforeAll(() => waitForDom());
beforeEach(() => {
container = dom.window.document.body;
});
afterEach(() => container = null)
it("should ", () => {
logDOM(container);
});