Sponsored Link
Looking for a unified way to  viewmonitor, and  debug Playwright Test Automation runs?
Try the Playwright Dashboard by today. Use the coupon code PWSOL10 for a 12 month 10% discount.

How do I test a website that has a page redirect with Playwright?

While working across different web applications you are bound to run into situations where a link or an action on a web page causes a redirect. This can happen via javascript or via http requests. A decent guide on ways redirects can be implemented can be found here.

Example Website With Redirect

In this solution, I've created a basic html page, with a Redirect button.

Simple webpage with redirect button

Once clicked it adds text to the page, starts a redirect and adds a Subscribe button to the page that pops up an alert. Below is the function. This code can be found within the public folder of this repo https://github.com/BMayhew/new-playwright-init/tree/master/public

function redirect() {
  // showing message before redirecting
  document.write("Redirecting to the url in 4 seconds...");

  // redirecting to a new page after 4 seconds
  setTimeout(function () {
    window.location = "https://playwrightsolutions.com/";
  }, 4000);

  const button = document.createElement("button");

  button.innerText = "Subscribe";
  button.addEventListener("click", () => {
    alert("Oh, you clicked me!");
  });
  document.body.appendChild(button);
}

Here is a recording of what happens when the Redirect button is clicked.

0:00
/
video of website redirect in action

Playwright Test that is failing

I wanted to make the exercise interesting, so on the redirect page I added a button with the text of Subscribe. Now when we write a test that visits the main page, clicks redirect, waits 4 seconds and then gets redirected to the PlaywrightSolutions.com homepage I want to click the Subscribe button and begin filling out information to become a subscriber.

import { test, expect } from "@playwright/test";

test("Visit Home Page and subscribe", async ({ page }) => {
  await page.goto("http://127.0.0.1:3000");
  await page.locator("text=Redirect").click();

  await page.locator("text=Subscribe").click();

  const popup = page.frameLocator('iframe[title="portal-popup"]');
  await popup.locator("id=input-name").fill("test");
  await popup.locator("text=Sign up").click();

  expect(popup.locator("text=Enter your email address")).toBeVisible;
});

When I execute this code I end up clicking on the Subscribe button on the redirect page not the Subscribe button on the PlaywrightSolutions.com page. So my script was failing on await popup.locator("id=input-name").fill("test");

Test Timeout error details

This is because as soon as the Redirect button was clicked, there was a locator on the redirect page that matched Subscribe button, so that locator was clicked, an alert popped up, and when the redirect finished there was no popup to input my name into.

page.waitForURL To The Rescue! (The Correct Answer)

There are a few different ways to solve this problem, but the easiest way is to utilize a method on page named waitForURL docs. This would be added after the Redirect button click and before the Subscribe button click. The waitForURL command can be full, using a glob patter, or regex pattern, more details can be found in the docs above. This solution was consistently passing when running multiple times with --repeat-each=10 command.

test("Visit Home Page and subscribe waitForURL", async ({ page }) => {
  await page.goto("http://127.0.0.1:3000");
  await page.locator("text=Redirect").click();

  await page.waitForURL("https://playwrightsolutions.com/");

  await page.locator("text=Subscribe").click();

  const popup = page.frameLocator('iframe[title="portal-popup"]');
  await popup.locator("id=input-name").fill("test");
  await popup.locator("text=Sign up").click();

  expect(popup.locator("text=Enter your email address")).toBeVisible;
});

page.waitForURL + a Javascript Promise

There is a bit more typing but if you run into a tricky scenario you can use a promise like below as called out in the documentation. One thing to note, is there is a method that has been deprecated called waitForNavigation(). It's best not to fool with this as it has race condition issues within your tests. I also found when using the promise such as this, I had too add a page.waitForLoadState()before I went to click the Subscribe button. The benefit to the first method and using await page.waitForURL() on its own is there is built in navigation, by default it waits until consider operation to be finished when the load event is fired

test("Visit Home Page and subscribe promise", async ({ page }) => {
  await page.goto("http://127.0.0.1:3000");

  // Start waiting for navigation before clicking. Note no await.
  const navigationPromise = page.waitForURL("https://playwrightsolutions.com/");
  
  // This action triggers the navigation with a script redirect.
  await page.locator("text=Redirect").click();
  await navigationPromise;

  await page.waitForLoadState("networkidle");
  await page.locator("text=Subscribe").click();

  const popup = page.frameLocator('iframe[title="portal-popup"]');
  await popup.locator("id=input-name").fill("test");
  await popup.locator("text=Sign up").click();

  expect(popup.locator("text=Enter your email address")).toBeVisible;
});

Alternate Promise solution using a Promise.all([]) array this waits for all conditions in the array to be met before fulfilling the promise.

test("Visit Home Page and subscribe promise", async ({ page }) => {
  await page.goto("http://127.0.0.1:3000");

  await Promise.all([
    // It is important to call waitForNavigation before click to set up waiting.
    page.waitForURL("https://playwrightsolutions.com/"),

    // Triggers a navigation with a script redirect.
    await page.locator("text=Redirect").click(),
  ]);

  await page.waitForLoadState("networkidle");
  await page.locator("text=Subscribe").click();

  const popup = page.frameLocator('iframe[title="portal-popup"]');
  await popup.locator("id=input-name").fill("test");
  await popup.locator("text=Sign up").click();

  expect(popup.locator("text=Enter your email address")).toBeVisible;
});

For the Lolz

Using expect().toPass() Anti-Pattern

I will start by saying DO NOT DO THIS, but I will show off that it is possible. With a recent release a .toPass() method was introduced which allows for retries to happen within a specific assertion block. You can hack this to wait for the page.url() to be the desired url before proceeding. One thing I had to do to make the below example less flakey was to add await page.waitForLoadState("networkidle"); line which waits for the network to stop for at least 500ms before proceeding, using this method doesn't guarantee that the DOM, and network requests are complete before proceeding. Again NOT RECOMMENDED but it is possible, you know for science!

test("Visit Home Page and subscribe anti-pattern", async ({ page }) => {
  await page.goto("http://127.0.0.1:3000");

  await page.locator("text=Redirect").click();

  await expect(async () => {
    expect(page.url()).toBe("https://playwrightsolutions.com/");
  }).toPass();

  await page.waitForLoadState("networkidle");
  await page.locator("text=Subscribe").click();

  const popup = page.frameLocator('iframe[title="portal-popup"]');
  await popup.locator("id=input-name").fill("test");
  await popup.locator("text=Sign up").click();

  expect(popup.locator("text=Enter your email address")).toBeVisible;
});

I do hope that this makes working with redirects easier, if you've made it this far 🙌! But I do hope you just focus on the best solution which is the first solution in the page.waitForURL To The Rescue section.


Thanks for reading! If you found this helpful, reach out and let me know on LinkedIn or consider buying me a cup of coffee. If you want more content delivered to you in your inbox subscribe below.