How to capture the window close event on Electron's renderer process? - javascript

On a classic front-end JavaScript, capturing the "window close" event can be done in a multiple ways. It can be done with:
// Method A: `close` event via an event listener
window.addEventListener( 'close', function(event){ /** your magic here **/ } )
// Method B: `beforeunload` event via an event listener
window.addEventListener( 'beforeunload', function(event){ /** your magic here **/ } )
// Method C: classic `onclose` event binder
window.onclose = function(event){ /** your magic here **/ }
// Method D: classic `onbeforeunload` event binder
window.onbeforeunload = function(event){ /** your magic here **/ }
I tried these methods inside the Electron's renderer script/environment but the window close event doesn't seem to get triggered.
Which brings to my question, how to capture the "window close" event on Electron's renderer process?

According to Electron's official documentation:
Event: "before-quit" Emitted before the application starts closing its windows. Calling event.preventDefault() will prevent the default behavior, which is terminating the application.
So you need to use the before-quit event on your Electron application.

Normally, proving that these events trigger (or not) is difficult as the render process would normally close prior to confirmation.
Instead, through the use of Inter-Process Communication, one can tell if any of these events trigger by sending one or more messages back to the main process for viewing.
To test this, I have implemented a simple preload.js, main.js and index.html script.
preload.js (main process)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld(
'ipcRender', {
send: (message) => {
ipcRenderer.send('closing', message);
}
});
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
let window;
function createWindow() {
const 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', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Testing
electronIpcMain.on('closing', (event, message) => {
console.log(message);
})
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
</head>
<body>
<input type="button" id="button" value="Close">
</body>
<script>
// Method A: `close` event via an event listener
window.addEventListener('close', (event) => {
window.ipcRender.send('Method A'); // Doesn't work
})
// Method B: `beforeunload` event via an event listener
window.addEventListener('beforeunload', (event) => {
window.ipcRender.send('Method B'); // Works
})
// Method C: classic `onclose` event binder
window.onclose = (event) => {
window.ipcRender.send('Method C'); // Doesn't work
}
// Method D: classic `onbeforeunload` event binder
window.onbeforeunload = (event) => {
window.ipcRender.send('Method D'); // Works
}
// Method E: `click' event via an event listener
document.getElementById('button').addEventListener('click', () => {
// Let's pretent that this can close the window from the main process
window.ipcRender.send('Method E');
})
</script>
</html>
Below is the result when closed via the traffic light close button or the task bar close menu.
Method B // `beforeunload` event via an event listener
Method D // classic `onbeforeunload` event binder
Depending on how one (correctly or incorrectly) implements their preload.js script, one may receive no results compared to the results shown above.
Whilst I usually implement my preload.js script differently to that shown above, I have kept it simple and inline with what most people appear to be familiar with. IE: Context Isolation (Enabled)

Related

How can Electron communicate with its Angular-Frontend?

I'm running an Angular Frontend within a frameless Electron-Window. Because the window is frameless, I need to implement the minimizing/maximizing/unmaximizing/closing-behaviour myself. I have a button for maximize and one for unmaximize and would like to hide one of them at all times, depending on the window state.
I have node-integration set to false and wonder how I can communicate from Electron to my Angular-Frontend. Then I would only need to find a way to get my app-window and emit an event, whenever it is maximized/unmaximized and then change my UI accordingly.
My communication from Angular to Electron works like this:
in Angular I have an 'electronService' which is injected in my components and calls the electron functions.
In my preload.js I expose a function from my main.js to my renderer-process like this:
const { ipcRenderer, contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
maximizeWindow: () => {
return ipcRenderer.invoke('electron::maximize-window');
}
});
And in my main.js I handle the incoming calls like this:
app.whenReady().then(() => {
ipcMain.handle('electron::maximize-window', maximizeWindow);
});
function maximizeWindow(_) {
...
}
Is there a way to do this in the opposite direction?
Though I don't use Angular, the implementation of your maximize / restore functionality is quite simple once laid out, understood and implemented correctly. The use of IPC and the detection and communication of your window state will be core to its functionality.
Desired functionality:
When the window is maximised, show the restore button.
When the window is not maximised (restored), show the maximise button.
Implementation:
Our render will show all four buttons: Minimise, restore, maximise and close.
On render button click, send an IPC message to the main process to implement window functionality.
Depending on the window state (change) in the main process, send a message to the render process to show / hide respective buttons.
On window creation, send a message to the render process to show / hide respective buttons.
Detect when the title bar is double-clicked to toggle render buttons between maximise and restore.
You may wonder why point 3 is needed? It exists due to points 4 and 5. Point 4 is used on window creation. No matter what settings you use to create your window (manually or pulled from a .json file), your newly created window will dynamically display the correct button.
Point 5 is when you double-click your title bar in and out of maximise / restore.
Within your preload.js script we need two functions. One to indicate which render button was click (point 2) and the other to receive the state of the window (point 3).
preload.js (main process)
const { ipcRenderer, contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
buttonClicked: (button) => {
ipcRenderer.send('buttonClicked', button);
},
windowState: (state) => {
ipcRenderer.on('windowState', state);
}
});
Within your main.js script we add functionality to tell the render process what state the window is in upon creation (point 4).
Here, we also receive the button click message from the render process (point 2) and implement functionality accordingly.
Lastly, listen for double-clicking of the title bar (point 5).
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
let window;
function initialiseButtons() {
if (window.isMaximized()) {
window.webContents.send('windowState','maximised')
} else {
window.webContents.send('windowState','restored')
}
}
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
frame: false,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
// .then(() => { window.maximize(); }) // Testing
.then(() => { initialiseButtons(); })
.then(() => { window.show(); });
// Double-click of title bar from restore to maximise
window.on('maximize', () => {
initialiseButtons();
})
// Double-click of title bar from maximise to restore
window.on('unmaximize', () => {
initialiseButtons();
})
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();
}
});
// ---
electronIpcMain.on('buttonClicked', (event, buttonClicked) => {
if (buttonClicked === 'minimise') {
window.minimize();
return;
}
if (buttonClicked === 'restore') {
window.restore();
window.webContents.send('windowState','restored');
return;
}
if (buttonClicked === 'maximise') {
window.maximize();
window.webContents.send('windowState','maximised');
return;
}
if (buttonClicked === 'close') {
window.close();
}
})
Lastly, we send the button click event(s) to the main process (point 1) and listen for the window state upon window creation (point 4).
For simplicity, i have overlooked the use of inline styling and buttons for interactivity.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body style="margin: 0; padding: 0;">
<div style="display: flex; flex-flow: row nowrap; padding: 5px; background-color: #ccc;">
<div style="flex: 1 0 auto">Title</div>
<div style="flex: 0 1 auto">
<input type="button" id="minimise" value="_">
<input type="button" id="restore" value="❐">
<input type="button" id="maximise" value="☐">
<input type="button" id="close" value="☓">
</div>
</div>
</body>
<script>
let restoreButton = document.getElementById('restore');
let maximiseButton = document.getElementById('maximise');
document.getElementById('minimise').addEventListener('click', () => {
window.electron.buttonClicked('minimise');
});
document.getElementById('close').addEventListener('click', () => {
window.electron.buttonClicked('close');
});
restoreButton.addEventListener('click', () => {
window.electron.buttonClicked('restore');
});
maximiseButton.addEventListener('click', () => {
window.electron.buttonClicked('maximise');
});
window.electron.windowState((event, state) => {
if (state === 'maximised') {
restoreButton.style.display = 'inline-block';
maximiseButton.style.display = 'none';
} else {
restoreButton.style.display = 'none';
maximiseButton.style.display = 'inline-block';
}
})
</script>
</html>

Is it possible to set event.isTrusted to true in JEST unit tests?

I am working with JavaScript events. As per the following MDN article, the event object has a flag called isTrusted. https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted
I have written a code which differentiates user events (triggered as a result of an actual user action) from programmatic events (triggered as a result of element.dispatchEvent(...). I cannot show the entire code but it goes something like below:
document.addEventListener('click', (event) => {
if (event.isTrusted) {
// ... client code follows
// somewhere in the code I am dispatching another custom event on body element
document.body.dispatchEvent(...);
}
});
I am trying to unit test this code in JEST (a popular unit testing library). Unfortunately we cannot simulate real click events since it's an automated code that does it for you. The the flag isTrusted is always set to false in JEST, thereby restricting me to test the client code.
I cannot change the value of isTrusted directly since it is a read only property. I am looking for ways where I can mock the event and set the isTrusted flag to true.
Edit: Added the test code:
describe('Event.isTrusted', () => {
beforeAll(() => {
document.body.dispatchEvent = jest.fn();
const btn = document.createElement('button');
btn.id = 'btn';
document.body.appendChild(btn);
});
...
it('should allow user events', () => {
const event = new MouseEvent('click', {
bubbles: true,
cancellable: true
});
document.getElementById('btn').dispatchEvent(event);
expect(document.body.dispatchEvent).toHaveBeenCalled();
});
});
Once again this is just a gist. I cannot show the client code. I hope this helps.
It is possible!
describe('Event.isTrusted', () => {
class CustomMouseEvent {
isTrusted = true;
// add the bubbles and cancellation here if needed
}
beforeAll(() => {
Object.defineProperty(global, 'MouseEvent', {
value: CustomMouseEvent,
});
document.body.dispatchEvent = jest.fn();
const btn = document.createElement('button');
btn.id = 'btn';
document.body.appendChild(btn);
});
...
it('should allow user events', () => {
const event = new CustomMouseEvent();
document.getElementById('btn').dispatchEvent(event);
expect(document.body.dispatchEvent).toHaveBeenCalled();
});
});

Angular 2+ detect closing window

i have troubles detecting a closing window after the build is done.
const newWindow = window.open(url, '_blank', options);
newWindow.onbeforeunload = () => null;
newWindow.addEventListener('beforeunload', (evt: BeforeUnloadEvent) =>
{
console.log(evt)
}
);
it works great until i do the build, there the beforeunload event does not get triggered. i also tried placing a host listener in the new window's component:
#HostListener('window:beforeunload', [ '$event' ])
beforeUnloadHander(event: BeforeUnloadEvent): void {
debugger;
}
but the same problem here. after the build is done, we don't arrive at the debugger anymore
anybody any idea what i am doing wrong? thanks for your help!
Edit Workaround
const heartBeatNewWindow = setInterval(() => {
if (newWindow.closed) {
this.canvasSettings.displayInNewWindow = false;
clearTimeout(heartBeatNewWindow);
}
}, 1500);
I had to do something similar and my approach was the following:
I created a generic catch from close event windows in the constructor of my service, them call method what handle this event. Inside this method I validate the origin of this event is the correct to execute the logic I needed. Look this example:
Inside the constructor:
if(window.addEventListener){
window.addEventListener("message", this.authService.handleMessage.bind(this), false);
}else{
(<any>window).attachEvent('onmessage', this.authService.handleMessage.bind(this));
}
And my method to handle that event:
handleMessage(event: Event) {
event.preventDefault();
const message = event as MessageEvent;
// Only trust messages from the below origin.
//
if ((message.origin !== environment.BASE_URL)) return;
const result = JSON.parse(message.data);
//Add your logic here
I Hope be helpfull.

Listening for Automatically-Triggered Client Events with page.on('customEvent', fn)

This gist seems to cover what I want to do, but it appears to be a thought experiment, rather than working code. In any case, I'm having trouble getting it to work for me.
I'm opening a page in PhantomJs that loads a JavaScript library and starts a process. When the process completes, that library triggers an event within the context of the instance object. I'd like to either
(a) set up PhantomJS to listen for the right event in the instance object in the client page
OR
(b) add some code to the client page that "bubbles-up" the event to window and set up PhantomJS to listen for that.
Here's what I've tried for option B.
client.html:
<html>
<body>
<script src='https://example.com/library.js'></script>
<script>
function sendWindowEvent() {
// I've also tried document, top.parent, etc.
window.dispatchEvent( new CustomEvent('myEvent', {
detail: {
message: "Hello World!",
time: new Date(),
},
bubbles: true,
cancelable: true
}));
}
var instance = new myLibrary.constructor();
instance.addEventListener("onInitialized", sendWindowEvent);
instance.start();
</script>
</body>
</html>
In node.js app:
const headlessBrowser = await phantom.create();
const page = await headlessBrowser.createPage();
await page.on('onResourceRequested', (requestData) => {
console.info('Requesting', requestData.url); // displays http://localhost:1337/client.html & https://example.com/library.js
});
await page.on('myEvent', async (evt) => {
console.log('event detected:', evt); // never triggered
});
const openStatus = await page.open('http://localhost:1337/client.html');
console.log('phantom status:', openStatus); // displays true
Any ideas on what I'm missing? Is this not a supported feature? Thanks in advance.
page.on event listeners are responding to specific technical events generated by PhantomJS, not by its target page. To receive native page events you will have to subscribe to them in the browser context:
await page.evaluate(function(){
window.addEventListener("myEvent", function(e){ console.log(e)})
});
Be sure to subscribe to page.onConsoleMessage callback to get that message.
window.callPhantom() was what I was looking for. Documentation is here.
client.html:
<html>
<body>
<script src='https://example.com/library.js'></script>
<script>
function sendWindowEvent() {
if (typeof window.callPhantom === 'function') {
window.callPhantom({hello: 'world'});
}
}
var instance = new myLibrary.constructor();
instance.addEventListener("onInitialized", sendWindowEvent);
instance.start();
</script>
</body>
</html>
In node.js app:
const headlessBrowser = await phantom.create();
const page = await headlessBrowser.createPage();
page.on('onCallback', data => {
console.log('CALLBACK: ' + JSON.stringify(data)); // Prints 'CALLBACK: {"hello":"world"}'
});
page.open('http://localhost:1337/client.html');

Event when window.location.href changes

I'm writing a Greasemonkey script for a site which at some point modifies location.href.
How can I get an event (via window.addEventListener or something similar) when window.location.href changes on a page? I also need access to the DOM of the document pointing to the new/modified url.
I've seen other solutions which involve timeouts and polling, but I'd like to avoid that if possible.
I use this script in my extension "Grab Any Media" and work fine ( like youtube case )
var oldHref = document.location.href;
window.onload = function() {
var bodyList = document.querySelector("body")
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
/* Changed ! your code here */
}
});
});
var config = {
childList: true,
subtree: true
};
observer.observe(bodyList, config);
};
With the latest javascript specification
const observeUrlChange = () => {
const oldHref = document.location.href;
const body = document.querySelector("body");
const observer = new MutationObserver(mutations => {
mutations.forEach(() => {
if (oldHref !== document.location.href) {
oldHref = document.location.href;
/* Changed ! your code here */
}
});
});
observer.observe(body, { childList: true, subtree: true });
};
window.onload = observeUrlChange;
Compressed with OpenAI
window.onload = () => new MutationObserver(mutations => mutations.forEach(() => oldHref !== document.location.href && (oldHref = document.location.href, /* Changed ! your code here */))).observe(document.querySelector("body"), { childList: true, subtree: true });
popstate event:
The popstate event is fired when the active history entry changes. [...] The popstate event is only triggered by doing a browser action such as a click on the back button (or calling history.back() in JavaScript)
So, listening to popstate event and sending a popstate event when using history.pushState() should be enough to take action on href change:
window.addEventListener('popstate', listener);
const pushUrl = (href) => {
history.pushState({}, '', href);
window.dispatchEvent(new Event('popstate'));
};
You can't avoid polling, there isn't any event for href change.
Using intervals is quite light anyways if you don't go overboard. Checking the href every 50ms or so will not have any significant effect on performance if you're worried about that.
There is a default onhashchange event that you can use.
Documented HERE
And can be used like this:
function locationHashChanged( e ) {
console.log( location.hash );
console.log( e.oldURL, e.newURL );
if ( location.hash === "#pageX" ) {
pageX();
}
}
window.onhashchange = locationHashChanged;
If the browser doesn't support oldURL and newURL you can bind it like this:
//let this snippet run before your hashChange event binding code
if( !window.HashChangeEvent )( function() {
let lastURL = document.URL;
window.addEventListener( "hashchange", function( event ) {
Object.defineProperty( event, "oldURL", { enumerable: true, configurable: true, value: lastURL } );
Object.defineProperty( event, "newURL", { enumerable: true, configurable: true, value: document.URL } );
lastURL = document.URL;
} );
} () );
Through Jquery, just try
$(window).on('beforeunload', function () {
//your code goes here on location change
});
By using javascript:
window.addEventListener("beforeunload", function (event) {
//your code goes here on location change
});
Refer Document : https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
Have you tried beforeUnload?
This event fires immediately before the page responds to a navigation request, and this should include the modification of the href.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD>
<TITLE></TITLE>
<META NAME="Generator" CONTENT="TextPad 4.6">
<META NAME="Author" CONTENT="?">
<META NAME="Keywords" CONTENT="?">
<META NAME="Description" CONTENT="?">
</HEAD>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
$(window).unload(
function(event) {
alert("navigating");
}
);
$("#theButton").click(
function(event){
alert("Starting navigation");
window.location.href = "http://www.bbc.co.uk";
}
);
});
</script>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#FF0000" VLINK="#800000" ALINK="#FF00FF" BACKGROUND="?">
<button id="theButton">Click to navigate</button>
Google
</BODY>
</HTML>
Beware, however, that your event will fire whenever you navigate away from the page, whether this is because of the script, or somebody clicking on a link.
Your real challenge, is detecting the different reasons for the event being fired. (If this is important to your logic)
Try this script which will let you run code whenever the URL changes (without a pageload, like an Single Page Application):
var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
if (location.href !== previousUrl) {
previousUrl = location.href;
console.log(`URL changed to ${location.href}`);
}
});
based on the answer from "Leonardo Ciaccio", modified code is here:
i.e. removed for loop and reassign the Body Element if it is removed
window.addEventListener("load", function () {
let oldHref = document.location.href,
bodyDOM = document.querySelector("body");
function checkModifiedBody() {
let tmp = document.querySelector("body");
if (tmp != bodyDOM) {
bodyDOM = tmp;
observer.observe(bodyDOM, config);
}
}
const observer = new MutationObserver(function (mutations) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
console.log("the location href is changed!");
window.requestAnimationFrame(checkModifiedBody)
}
});
const config = {
childList: true,
subtree: true
};
observer.observe(bodyDOM, config);
}, false);
Well there is 2 ways to change the location.href. Either you can write location.href = "y.html", which reloads the page or can use the history API which does not reload the page. I experimented with the first a lot recently.
If you open a child window and capture the load of the child page from the parent window, then different browsers behave very differently. The only thing that is common, that they remove the old document and add a new one, so for example adding readystatechange or load event handlers to the old document does not have any effect. Most of the browsers remove the event handlers from the window object too, the only exception is Firefox. In Chrome with Karma runner and in Firefox you can capture the new document in the loading readyState if you use unload + next tick. So you can add for example a load event handler or a readystatechange event handler or just log that the browser is loading a page with a new URI. In Chrome with manual testing (probably GreaseMonkey too) and in Opera, PhantomJS, IE10, IE11 you cannot capture the new document in the loading state. In those browsers the unload + next tick calls the callback a few hundred msecs later than the load event of the page fires. The delay is typically 100 to 300 msecs, but opera simetime makes a 750 msec delay for next tick, which is scary. So if you want a consistent result in all browsers, then you do what you want to after the load event, but there is no guarantee the location won't be overridden before that.
var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");
var callBacks = [];
var uglyHax = function (){
var done = function (){
uglyHax();
callBacks.forEach(function (cb){
cb();
});
};
win.addEventListener("unload", function unloadListener(){
win.removeEventListener("unload", unloadListener); // Firefox remembers, other browsers don't
setTimeout(function (){
// IE10, IE11, Opera, PhantomJS, Chrome has a complete new document at this point
// Chrome on Karma, Firefox has a loading new document at this point
win.document.readyState; // IE10 and IE11 sometimes fails if I don't access it twice, idk. how or why
if (win.document.readyState === "complete")
done();
else
win.addEventListener("load", function (){
setTimeout(done, 0);
});
}, 0);
});
};
uglyHax();
callBacks.push(function (){
console.log("cb", win.location.href, win.document.readyState);
if (win.location.href !== "http://localhost:4444/y.html")
win.location.href = "http://localhost:4444/y.html";
else
console.log("done");
});
win.location.href = "http://localhost:4444/x.html";
If you run your script only in Firefox, then you can use a simplified version and capture the document in a loading state, so for example a script on the loaded page cannot navigate away before you log the URI change:
var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");
var callBacks = [];
win.addEventListener("unload", function unloadListener(){
setTimeout(function (){
callBacks.forEach(function (cb){
cb();
});
}, 0);
});
callBacks.push(function (){
console.log("cb", win.location.href, win.document.readyState);
// be aware that the page is in loading readyState,
// so if you rewrite the location here, the actual page will be never loaded, just the new one
if (win.location.href !== "http://localhost:4444/y.html")
win.location.href = "http://localhost:4444/y.html";
else
console.log("done");
});
win.location.href = "http://localhost:4444/x.html";
If we are talking about single page applications which change the hash part of the URI, or use the history API, then you can use the hashchange and the popstate events of the window respectively. Those can capture even if you move in history back and forward until you stay on the same page. The document does not changes by those and the page is not really reloaded.
ReactJS and other SPA applications use the history object
You can listen to window.history updating with the following code:
function watchHistoryEvents() {
const { pushState, replaceState } = window.history;
window.history.pushState = function (...args) {
pushState.apply(window.history, args);
window.dispatchEvent(new Event('pushState'));
};
window.history.replaceState = function (...args) {
replaceState.apply(window.history, args);
window.dispatchEvent(new Event('replaceState'));
};
window.addEventListener('popstate', () => console.log('popstate event'));
window.addEventListener('replaceState', () => console.log('replaceState event'));
window.addEventListener('pushState', () => console.log('pushState event'));
}
watchHistoryEvents();

Categories