Chrome-remote-interface: Question: Can I redefine the `Page.loadEventFired` event after it's been fired?

Created on 19 Apr 2017  路  13Comments  路  Source: cyrus-and/chrome-remote-interface

I want to submit a login form, then redefine Page.loadEventFired again and do something else when the new page loads. Something like this:

  Page.loadEventFired(() => {
    Promise.resolve()
    .then(function() {
      return Runtime.evaluate(/* expression here will submit login form */)
    })
    .then(function (response) {
      // probably not necessary once things are working
      console.log('response:', response);
    })
    .then(/* redefine Page.loadEventFired here */)
    .then(function() {
      return Page.navigate({
        url: 'second link after login',
      });
    })

// I'm not sure where we should finish logic so we can handle a load event again but
// something happens in between here and I take a screenshot after:

... Page.loadEventFired...
    .then(Page.captureScreenshot)
    .then(screenshot)
    .then(function() {
      client.close();
    });
  });

Looked around for an example and couldn't find one.

question

Most helpful comment

Here we go, since [v0.20.0] you can also write:

const CDP = require('chrome-remote-interface');

CDP(async (client) => {
    const {Network, Page} = client;

    try {
        await Page.enable();

        console.log('stage 0');
        await Page.navigate({url: 'https://example.com'});
        await Page.loadEventFired();

        console.log('stage 1');
        await Page.navigate({url: 'https://example.com/1'});
        await Page.loadEventFired();

        console.log('stage 2');
        await Page.navigate({url: 'https://example.com/2'});
        await Page.loadEventFired();

        console.log('stage 3');
        await Page.navigate({url: 'https://example.com/3'});
        await Page.loadEventFired();

        console.log('done');
    } catch (err) {
        console.error(err);
    }

    client.close();
}).on('error', (err) => {
    console.error(err);
});

Any feedback is welcome.

All 13 comments

You can manage events as explained in #93 or use a single listener but keep a state variable. Assuming that the login form redirects to another page (thus triggering a third Page.loadEventFired), you'll need something like:

async function doStuff(client) {
    const {Page} = client;
    let state = 0;

    Page.loadEventFired(() => {
        switch (state) {
        case 0:
            console.log('login page loaded, now submit the form');
            state++;
            break;
        case 1:
            console.log('login performed, now load the screenshot page');
            state++;
            break;
        case 2:
            console.log('take the screenshot then close');
            break;
        }
    });

    await Page.navigate({url: loginUrl});
}

Probably the former approach will result in a more elegant code, it really depends.

@cyrus-and thanks, I'll try this out when I get off from work today and close the issue if no other questions.

I went with the switch-case method because removing the listeners required me to unbind within Page.loadEventFired, and re-define it within the parent Page.loadEventFired. After a few of these, you'd be nested in pretty deep. Let me know if I'm doing this wrong

Page.loadEventFired(() => {
  // do something
  client.removeListener('Page.loadEventFired');
  Page.loadEventFired(() => {
    // do something else
      client.removeListener('Page.loadEventFired');
      Page.loadEventFired(() => {
        // do something else again
      })
      Page.navigate('third page')
  })
  Page.navigate('second page)
});
Page.navigate('first page')

You can flatten the code like this:

const CDP = require('chrome-remote-interface');

CDP(async (client) => {
    const {Network, Page} = client;

    const start = () => {
        console.log('stage 0');
        client.once('Page.loadEventFired', stage1);
        Page.navigate({url: 'https://example.com'});
    };

    const stage1 = () => {
        console.log('stage 1');
        client.once('Page.loadEventFired', stage2);
        Page.navigate({url: 'https://example.com/1'});
    };

    const stage2 = () => {
        console.log('stage 2');
        client.once('Page.loadEventFired', stage3);
        Page.navigate({url: 'https://example.com/2'});
    };

    const stage3 = () => {
        console.log('stage 3');
        client.once('Page.loadEventFired', end);
        Page.navigate({url: 'https://example.com/3'});
    };

    const end = () => {
        client.close();
        console.log('done');
    };

    Page.enable().then(start);
}).on('error', (err) => {
    console.error(err);
});

Notice the use of once, which is equivalent to use on then removeListener.

I plan to add a Promise interface for once events which should allow to cope with these use cases in a more elegant way. I'll write here when it's done.

Here we go, since [v0.20.0] you can also write:

const CDP = require('chrome-remote-interface');

CDP(async (client) => {
    const {Network, Page} = client;

    try {
        await Page.enable();

        console.log('stage 0');
        await Page.navigate({url: 'https://example.com'});
        await Page.loadEventFired();

        console.log('stage 1');
        await Page.navigate({url: 'https://example.com/1'});
        await Page.loadEventFired();

        console.log('stage 2');
        await Page.navigate({url: 'https://example.com/2'});
        await Page.loadEventFired();

        console.log('stage 3');
        await Page.navigate({url: 'https://example.com/3'});
        await Page.loadEventFired();

        console.log('done');
    } catch (err) {
        console.error(err);
    }

    client.close();
}).on('error', (err) => {
    console.error(err);
});

Any feedback is welcome.

That's a lot more elegant than the switch-case method. Thanks.

I know this is closed and all but I found it plenty useful. After trying some of the approaches mentioned here, I went with using a function hoisted as a state variable. In doing so I can await multiple page loads in a single function by resolving a Promise by the page load.

const CDP = require('chrome-remote-interface');
let pageLoad = clickSomeActionThenSubmitForm;

function clickSomeActionThenSubmitForm(Runtime) {
    const clickSomeAction = 'document.querySelector(\'a[href="#someAction"]\').click()';
    const setSomeInfo = 'document.querySelector("input#someinfo").value = "info"';
    const submit = 'document.querySelector(\'form[action*="/smilesAndJoy"] button\').click()';

    return Runtime
        .evaluate({ expression: clickSomeAction })
        .then(() => { return new Promise(resolve => { pageLoad = resolve; }) })
        .then(() => { return Runtime.evaluate({expression: setSomeInfo })})
        .then(() => { return Runtime.evaluate({expression: submit })})
        .then(() => { return new Promise(resolve => { pageLoad = resolve; }) });
}
 ...
Page.loadEventFired(() => { pageLoad(Runtime) }); 

@jmrnilsson this is good information, thanks.

Got a question related to this one, hope you can find the time to answer.

I was implementing something similar to this to handle pages that trigger a reload the first time a user visits a website thus has no cookies for it. That's the case of behance for example, where they trigger a page reload with JavaScript (so there's no 301, just a 200 and after some time I guess some JS code triggers a reload).

I was wondering if you know of a way to keep the cookies between the first request and the other. I still get a "Promise was collected" in the first request but I can ignore that and try to execute the same promise in the second request if only I could tell Chrome not to reset everything.

Thanks!

I was wondering if you know of a way to keep the cookies between the first request and the other.

@fracasula that should be the default behavior, cookies are not dropped between subsequent requests. Anyway this is the recommended version.

The "Promise was collected" error is discussed in #116 and this FAQ entry.

I see. Maybe I'm not getting any cookie because the first request fails with a "Promise was collected" error. Can't think of a way of making Chrome just wait till the website triggers the JS reload itself though 馃

Anyway thanks for your help, much appreciated.

@fracasula if you are able to come up with a minimal example feel free to file a new issue. Requests don't fail with a "Promise was collected" error; Runtime.evaluate does.

@cyrus-and I reproduced this in a simple script and opened a new issue here https://github.com/cyrus-and/chrome-remote-interface/issues/231

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Bnaya picture Bnaya  路  4Comments

fracasula picture fracasula  路  6Comments

DavidXanatos picture DavidXanatos  路  3Comments

anzeznidarsic picture anzeznidarsic  路  8Comments

sergueyarellano picture sergueyarellano  路  6Comments