Navigation and loading

Playwright logically splits the process of showing a new document in the page into navigation and loading.

Navigation

Page navigation can be either initiated by the Playwright call:

  1. // Load a page
  2. await page.goto('https://example.com');
  3. // Reload a page
  4. await page.reload();
  5. // Click a link
  6. await page.click('text="Continue"');

or by the page itself:

  1. // Programmatic navigation
  2. window.location.href = 'https://example.com';
  3. // Single page app navigation
  4. history.pushState({}, 'title', '#deep-link');

Navigation intent may result in being canceled, for example transformed into a download or hitting an unresolved DNS address. Only when the navigation succeeds, page starts loading the document.

Loading

Page load takes time retrieving the response body over the network, parsing, executing the scripts and firing the events. Typical load scenario goes through the following load states:

  • page.url() is set to the new url
  • document content is loaded over network and parsed
  • domcontentloaded event is fired
  • page executes some scripts and loads resources like stylesheets and images
  • load event is fired
  • page executes dynamically loaded scripts
  • networkidle is fired - no new network requests made for at least 500 ms

Common scenarios

By default, Playwright handles navigations seamlessly so that you did not need to think about them. Consider the following scenario, where everything is handled by Playwright behind the scenes:

  1. await page.goto('http://example.com');
  2. // If the page does a client-side redirect to 'http://example.com/login'.
  3. // Playwright will automatically wait for the login page to load.
  4. // Playwright waits for the lazy loaded #username and #password inputs
  5. // to appear before filling the values.
  6. await page.fill('#username', 'John Doe');
  7. await page.fill('#password', '********');
  8. // Playwright waits for the login button to become enabled and clicks it.
  9. await page.click('text=Login');
  10. // Clicking the button navigates to the logged-in page and Playwright
  11. // automatically waits for that.

Explicit loading handling may be required for more complicated scenarios though.

Loading a popup

When popup is opened, explicitly calling page.waitForLoadState() ensures that popup is loaded to the desired state.

  1. const [ popup ] = await Promise.all([
  2. page.waitForEvent('popup'),
  3. page.click('a[target="_blank"]'), // <-- opens popup
  4. ]);
  5. await popup.waitForLoadState('load');
  6. await popup.evaluate(() => window.globalVariableInitializedByOnLoadHandler);

Unusual client-side redirects

Usually, the client-side redirect happens before the load event, and page.goto() method automatically waits for the redirect. However, when redirecting from a link click or after the load event, it would be easier to explicitly waitForNavigation() to a specific url.

  1. await Promise.all([
  2. page.waitForNavigation({ url: '**/login' }),
  3. page.click('a'), // Triggers a navigation with a script redirect.
  4. ]);

Notice the Promise.all to click and wait for navigation. Awaiting these methods one after the other is racy, because navigation could happen too fast.

Click triggers navigation after a timeout

When onclick handler triggers a navigation from a setTimeout, use an explicit waitForNavigation() call as a last resort.

  1. await Promise.all([
  2. page.waitForNavigation(), // Waits for the next navigation.
  3. page.click('a'), // Triggers a navigation after a timeout.
  4. ]);

Notice the Promise.all to click and wait for navigation. Awaiting these methods one after the other is racy, because navigation could happen too fast.

Unpredictable patterns

When the page has a complex loading pattern, the custom waiting function is most reliable.

  1. await page.goto('http://example.com');
  2. await page.waitForFunction(() => window.amILoadedYet());
  3. // Ready to take a screenshot, according to the page itself.
  4. await page.screenshot();

When clicking on a button triggers some asynchronous processing, issues a couple GET requests and pushes a new history state multiple times, explicit waitForNavigation() to a specific url is the most reliable option.

  1. await Promise.all([
  2. page.waitForNavigation({ url: '**/invoice#processed' }),
  3. page.click('text=Process the invoice'), // Triggers some complex handling.
  4. ]);

Lazy loading, hydration

TBD