javascriptnode.jsgoogle-chromedialogpuppeteer

Can I ignore the "Leave Site?" dialog when browsing headless using Puppeteer?


I am testing some forms on a website using Puppeteer. When I run the tests, I noticed the execution gets stuck when trying to navigate between pages. I did a dry run myself manually and realised that the page is issuing a dialog as some of the form inputs have changed without the form being submitted.

It's the message that says:

Leave site?
Changes you made may not be saved.

There are some obvious workarounds to this like I could ensure that the form is submitted each time before navigating to the next page. However, I'd ideally like to just be able to ignore this dialog altogether as I'm just running tests and I don't care that the changes won't be saved.

Is there a way to disable these messages? If not, is there a way to check if there's a dialog open and then dismiss it?


Solution

  • To elaborate on the existing answer, the beforeunload event you're trying to deal with causes a dialog box prompt to appear. This dialog can be handled in the same manner as prompts and alerts, with page.on("dialog", dialog => ...).

    The dialog object passed to the handler can be checked for the "beforeunload" type, then you can call dismiss() or accept() depending on the behavior you want. dismiss() stays on the current page, aborting the navigation action, while accept() agrees to leave the site.

    Your likely solution is to agree to leave the site with accept():

    import puppeteer from "puppeteer"; // ^23.6.0
    
    let browser;
    (async () => {
      const html = `
        <h1>a page that prompts before unload</h1>
        <script>
        window.addEventListener("beforeunload", function (e) {
          e.preventDefault();
          (e || window.event).returnValue = "";
          return "";
        });
        </script>
      `;
      browser = await puppeteer.launch({headless: false});
      const [page] = await browser.pages();
    
      const acceptBeforeUnload = dialog => 
        dialog.type() === "beforeunload" && dialog.accept();
      page.on("dialog", acceptBeforeUnload); 
    
      await page.setContent(html);
    
      await page.goto("https://www.example.com");
      console.log(page.url()); // => => https://www.example.com
    })()
      .catch(err => console.error(err))
      .finally(() => browser?.close());
    

    Note that using the existing answer's method of simply setting window.beforeunload = null in an evaluate callback doesn't work for this page. You can test that by removing the page.on and adding the evaluate call above page.goto.

    However, for more complex behaviors, like enabling and disabling the unload dynamically, the evaluate approach seems to help avoid abort throws in conjunction with removing a dismiss handler, as this example illustrates:

    import puppeteer from "puppeteer";
    
    let browser;
    (async () => {
      const html = `
        <h1>a page that prompts before unload</h1>
        <script>
        window.addEventListener("beforeunload", function (e) {
          e.preventDefault();
          (e || window.event).returnValue = "";
          return "";
        });
        </script>
      `;
      browser = await puppeteer.launch({headless: false});
      const [page] = await browser.pages();
    
      const dismissBeforeUnload = dialog =>
        dialog.type() === "beforeunload" && dialog.dismiss();
      page.on("dialog", dismissBeforeUnload);
    
      await page.setContent(html);
    
      // throws Error: net::ERR_ABORTED at https://www.example.com
      await page.goto("https://www.example.com").catch(() => {});
      console.log(page.url()); // => about:blank
    
      // add to avoid Error: net::ERR_ABORTED when adding a new handler
      await page.evaluate(() => {
        window.onbeforeunload = null;
      });
      page.off("dialog", dismissBeforeUnload);
    
      // next unload, we'll accept the dialog
      page.on("dialog", dialog =>
        dialog.type() === "beforeunload" && dialog.accept()
      );
    
      // this navigation will succeed
      await page.goto("https://www.example.com");
      console.log(page.url()); // => https://www.example.com
    })()
      .catch(err => console.error(err))
      .finally(() => browser?.close());