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.
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.
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");
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;
});
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.