21

Is it possible to listen to events dispatched by in-page objects? Let's say I have this code in the page I go to:

var event = new CustomEvent('status', { detail: 'ok' });
window.addEventListener('status', function(e) {
  console.log('status: ', e.detail);
});
setInterval(window.dispatchEvent, 1000, event);

I'd like to be able to listen to events dispatched by the window object (or any other JS object for that matter). How can I do this in Puppeteer?

Vic
  • 211
  • 1
  • 2
  • 5
  • 4
    There's an example of listening for a custom event in the repo: https://github.com/GoogleChrome/puppeteer/blob/master/examples/custom-event.js – ebidel Nov 09 '17 at 17:50
  • Thank you! that's exactly what I needed. – Vic Nov 10 '17 at 20:41

3 Answers3

23

First, you have to expose a function that can be called from within the page. Second, you listen for the event and call the exposed function and pass on the event data.

// Expose a handler to the page
await page.exposeFunction('onCustomEvent', ({ type, detail }) => {
    console.log(`Event fired: ${type}, detail: ${detail}`);
});

// listen for events of type 'status' and
// pass 'type' and 'detail' attributes to our exposed function
await page.evaluateOnNewDocument(() => {
    window.addEventListener('status', ({ type, detail }) => {
        window.onCustomEvent({ type, detail });
    });
});

await page.goto(/* ... */);

If there is an event status fired in the page now (like the code you have given), you would see it being logged in the console of your Node.js application.

As posted in the comments, a more complex example can be found in the puppeteer examples.

Thomas Dondorf
  • 23,416
  • 6
  • 84
  • 105
  • Hi there. Following the simple approach explained in here, I call in my static html, at the end of my lengthy page render: var event = new Event("sceneReady"); window.dispatchEvent(event); but the event listener is never reached at the NodeJS code counterpart. – MAML Apr 05 '21 at 20:19
  • To listen to `event`s fired on `document` use `document.addEventListener()` instead of `window.addEventListener()` in the `page.evaluateOnNewDocument()` method. – Darkosphere Oct 07 '22 at 18:58
8

A simpler method await waitForEvent('event-name', 10)

/**
 * Wait for the browser to fire an event (including custom events)
 * @param {string} eventName - Event name
 * @param {integer} seconds - number of seconds to wait.
 * @returns {Promise} resolves when event fires or timeout is reached
 */
async function waitForEvent(eventName, seconds) {

    seconds = seconds || 30;

    // use race to implement a timeout
    return Promise.race([

        // add event listener and wait for event to fire before returning
        page.evaluate(function(eventName) {
            return new Promise(function(resolve, reject) {
                document.addEventListener(eventName, function(e) {
                    resolve(); // resolves when the event fires
                });
            });
        }, eventName),

        // if the event does not fire within n seconds, exit
        page.waitForTimeout(seconds * 1000)
    ]);
}

Source

John Doherty
  • 3,669
  • 36
  • 38
  • 1
    Best answer IMO – JulianSoto Mar 05 '21 at 06:06
  • How do you use it? I'm doing: ``` await page.goto('http://localhost:1234'); await waitForEvent('myCustomEvent'); ``` I can add a custom event handler and see my event is definitely getting fired, but this promise never resolves. – johnboiles Mar 09 '21 at 19:23
  • 1
    Nevermind, I was using `window.dispatchEvent` to fire my event and not `document.dispatchEvent` – johnboiles Mar 09 '21 at 19:59
  • One glitch I ran into that was non-obvious to me: It looks like `Promise.race` doesn't cancel the `waitForTimeout` promise. This means that if you have a 30 second timeout in `waitForEvent` then your puppeteer script will run for at least 30 seconds while it waits for the timeout promise to resolve even if the event fired. I didn't see an obvious/simple way to fix this so I've just stopped using the timeout in my script. – johnboiles Mar 10 '21 at 04:38
6

If you want to actually await the custom event, you can do it this way.

const page = await browser.newPage();

/**
  * Attach an event listener to page to capture a custom event on page load/navigation.
  * @param {string} type Event name.
  * @return {!Promise}
  */
function addListener(type) {
  return page.evaluateOnNewDocument(type => {
    // here we are in the browser context
    document.addEventListener(type, e => {
      window.onCustomEvent({ type, detail: e.detail });
    });
  }, type);
}

const evt = await new Promise(async resolve => {
  // Define a window.onCustomEvent function on the page.
  await page.exposeFunction('onCustomEvent', e => {
    // here we are in the node context
    resolve(e); // resolve the outer Promise here so we can await it outside
  });
  await addListener('status'); // setup listener for "status" custom event on page load
  await page.goto('http://example.com');  // N.B! Do not use { waitUntil: 'networkidle0' } as that may cause a race condition
});

console.log(`${evt.type} fired`, evt.detail || '');

Built upon the example at https://github.com/puppeteer/puppeteer/blob/main/examples/custom-event.js

mflodin
  • 1,093
  • 1
  • 12
  • 22