mswjs / msw

Industry standard API mocking for JavaScript.
https://mswjs.io
MIT License
15.94k stars 518 forks source link

My app hooks in beforeunload with a confirmation message and then MSW stops working #1105

Open jmverges opened 2 years ago

jmverges commented 2 years ago

Describe the bug

I have some code in my page that hooks in beforeunload and then the user can cancel and stay in the page or leave the page. If the user decides to stay in the page msw gets broken.

Environment

Please also provide your browser version.

Firefox 98.0b

To Reproduce

Steps to reproduce the behavior:

Add some code like:

window.addEventListener("beforeunload", function(e) {
      const confirmationMessage = "Msw will not work from this point";
      e.returnValue = confirmationMessage;     // Gecko, Trident, Chrome 34+
      return confirmationMessage;              // Gecko, WebKit, Chrome <34
});

Cancel and remain in the page Trigger some mocked mutation

Expected behavior

Mutation should be triggered

Screenshots

I can go to .vite generated code and delete those lines and then the mutation is triggered.

image
kettanaito commented 2 years ago

Hey, @jmverges. Thanks for reporting this.

We have a listener on the beforeunload event that signals the worker that the current client is being closed:

https://github.com/mswjs/msw/blob/8d1fcdb7b55b44977d9b9c6b05d85289eae53290/src/setupWorker/start/createStartHandler.ts#L64-L75

I think despite you preventing the unload in your listener, the one from the library is still getting called.

You may want to consider using event.stopPropagation() in your listener:

window.addEventListener("beforeunload", function(e) {
      const confirmationMessage = "Msw will not work from this point";
      e.returnValue = confirmationMessage;
+     // Prevent other listeners on "beforeunload" from executing.
+     e.stopPropagation();
      return confirmationMessage;
});

Since you're likely adding your listener after MSW has done so, yours will execute first, so you're able to control whether the beforeunload event propagates down the listeners chain.

Please, give this a try and let me know.

jmverges commented 2 years ago

Thanks for your quick response @kettanaito As soon as I read it I though it would work, however after I gave it a try, the 'beforeunload' from MSW is executed before mine. Not sure if I can handle the order of the events from my code. I'm registering msw as a plugin in nuxt3 I though this would be executed before my page but, maybe is not?

Ryomasao commented 2 years ago

I'm having the same problem.

The order beforeunloads are executed is the order in which they are registered, so msw's beforeunload is executed first before e.stopImmediatePropagation or e.stopPropagation is executed in the application.

Is there any way to do this? Like restarting the worker when beforeunloading?

UPDATE

I tried to start the worker again beforeunload, but the following error occurred.

image

Machineric commented 2 years ago

I'm experiencing the same issue with the author. Has anyone solved this issue?

florianmatz commented 1 year ago

Same here :P - as described evt.preventDefault() is not working, because it is to late. Any updates?

Dodobrat commented 2 months ago

I think i figured a way to do it. I have a React app and it is a little unconventional for it.

NOTE: I am not sure if this will work for SSR apps or lazy loaded modules. I think if this is not included in the initial js bundle it wouldn't work.

Add an IIFE outside a component, so it mounts immediately on js load. As it loads only once, we don't even need to remove the event listener.

(function () {
  const handleOnBeforeUnload = (e: BeforeUnloadEvent) => {
    const getIsDirty = () => Boolean(sessionStorage.getItem("isDirty"));
    if (!getIsDirty()) return;
    e.stopImmediatePropagation();
    e.preventDefault();
  };

  window.addEventListener("beforeunload", handleOnBeforeUnload, {
    capture: true,
  });
})();

Inside a component you can update the session storage in order be able to show the page reload confirmation

useEffect(() => {
  sessionStorage.setItem("isDirty", isDirty ? "1" : "");
  return () => sessionStorage.removeItem("isDirty");
}, [isDirty]);

Doing it this way, allows the event listener to be attached before the msw one, so doing e.stopImmediatePropagation() means we can trigger our code before the msw one.

Screenshot 2024-09-14 at 15 54 03