QA Graphic
April 24, 2025

Speed Up Your Tests with storageState()

Little Known Tip to Get Your Test Running Faster

Tired of logging in before every test? Playwright’s storageState() lets you save your login state and reuse it across multiple tests - boosting performance and reliability.

Quick Tip: Reusing browser context cuts down execution time and enhances CI/CD pipeline efficiency.

Why storageState() Matters

Every time your test logs in, you're spending time and creating potential points of failure. Also it adds additional overhead on your tests - that may impact the overall test time. Instead, you can log in once, save the state (cookies + local storage), and load that state in future tests.

Step 1: Save the Auth State

Create a setup script that logs in and saves the storage state.

// setup-auth.ts
import { chromium } from '@playwright/test';
(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com/login');
  await page.fill('#username', 'your-username');
  await page.fill('#password', 'your-password');
  await page.click('button[type="submit"]');
  // Wait for redirect or element that confirms login
  await page.waitForSelector('#dashboard');
  // Save storage state to file
  await context.storageState({ path: 'auth.json' });
  await browser.close();
})();

Step 2: Reuse the Auth State

In your test file, load the previously saved auth.json file to reuse the session.

// example.spec.ts
import { test, expect } from '@playwright/test';
test.use({
  storageState: 'auth.json'
});
test('Dashboard loads for authenticated user', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  await expect(page.locator('#dashboard')).toBeVisible();
});

When Should You Use This?

  • Tests that require an authenticated user
  • Reducing login redundancy in CI/CD pipelines
  • Speeding up test suites with shared session state
Pro Tip: You can use different storage files for different roles or test scenarios.

Start using storageState() today and take the fast lane through your E2E tests!

Permalink
April 17, 2025

Modern Number Formatting

From Perl to Playwright

The Old Way: Perl's Commify Function

In the days of Perl, developers often wrote custom functions to format numbers with commas for readability. Here's an example of a Perl function called commify that added commas to numbers:


sub commify {
    local($_) = shift;
    1 while s/^(-?d+)(d{3})/$1,$2/;
    return $_;
}

                

This function:

  • Takes a number as input.
  • Uses a regular expression to insert commas every three digits from the right.
  • Handles negative numbers with the -? pattern.
  • Returns the formatted string (e.g., 1234567 becomes 1,234,567).

While effective, this approach is verbose and relies on regex, which can be error-prone and hard to maintain.

The Modern Way: Playwright with TypeScript

Today, when working with modern web automation tools like Playwright and TypeScript, you can achieve the same result using JavaScript's built-in toLocaleString() method. This method is simpler, more readable, and supports internationalization out of the box.

Here's how you can format numbers in a Playwright test with TypeScript:


import { test, expect } from '@playwright/test';
test('Format number with commas', async ({ page }) => {
    await page.goto('https://example.com');
    const number = 1234567;
    const formattedNumber = number.toLocaleString('en-US'); // Outputs: "1,234,567"
    // Example: Set the formatted number in an input field
    await page.locator('#number-input').fill(formattedNumber);
    // Verify the value
    const inputValue = await page.locator('#number-input').inputValue();
    expect(inputValue).toBe('1,234,567');
});

In this example:

  • toLocaleString('en-US') formats the number with commas according to the US locale (e.g., 1234567 becomes 1,234,567).
  • The formatted number is used in a Playwright test to fill an input field and verify its value.
  • No custom regex or loops are needed, making the code cleaner and less prone to errors.

Why toLocaleString() is Better

Using toLocaleString() over the Perl commify function offers several advantages:

  • Simplicity: No need for complex regex or manual string manipulation.
  • Internationalization: Supports different locales (e.g., de-DE for German formatting with periods: 1.234.567).
  • Built-in: Native to JavaScript, so no custom code is required.
  • Type Safety: When used in TypeScript, you get type checking for numbers and locale strings.

For example, to format a number for a German audience:


const number = 1234567;
const formattedNumber = number.toLocaleString('de-DE'); // Outputs: "1.234.567"

                

Using toLocaleString() in Playwright

In a real-world Playwright scenario, you might need to format numbers when testing forms, displaying data, or verifying UI elements. Here's a more complete example:


import { test, expect } from '@playwright/test';
test('Test number formatting in a form', async ({ page }) => {
    await page.goto('https://example.com/form');
    const rawNumber = 9876543.21;
    const formattedNumber = rawNumber.toLocaleString('en-US', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
    }); // Outputs: "9,876,543.21"
    // Fill a form input with the formatted number
    await page.locator('#price-input').fill(formattedNumber);
    // Submit the form
    await page.locator('#submit-btn').click();
    // Verify the displayed result
    const displayText = await page.locator('#price-display').textContent();
    expect(displayText).toContain('9,876,543.21');
});

This test formats a number with two decimal places, fills a form, submits it, and verifies the result in the UI.

Conclusion

The transition from Perl's commify to JavaScript's toLocaleString() in a Playwright and TypeScript environment showcases how modern web development simplifies tasks that once required custom solutions. With toLocaleString(), you get a robust, maintainable, and internationally aware solution for number formatting, perfectly suited for browser automation with Playwright.

So, next time you're formatting numbers in your Playwright tests, skip the regex and reach for toLocaleString() - your code will thank you!

Permalink
April 10, 2025

Negative Testing in Playwright with TypeScript

Using Try/Catch

One of the key ways of building a champion automated test suite is negative testing - making sure that your application gracefully handles errors, exceptions, and bad user behavior. This is a good way for automation to add value with testing as negative testing isn't something that manual QA does on a regular bases.

When working with Playwright and TypeScript, there isn't a direct equivalent to pytest.raises() in Python for expecting exceptions. But that doesn't mean you're out of luck.

With TypeScript, the tried-and-true try/catch pattern becomes a powerful way to validate that your code throws errors when it's supposed to.

Scenario: Navigating to an Invalid URL

Say you want to verify that navigating to a malformed URL triggers the appropriate error. Here's how you can do it using Playwright with TypeScript:

import { test, expect } from '@playwright/test';
test('should throw an error when navigating to an invalid URL', async ({ playwright }) => {
  let errorCaught = false;
  let errorMessage = '';
  try {
    // Attempt to navigate to an invalid URL, which should throw an error
    await playwright.chromium.launch().then(async browser => {
      const page = await browser.newPage();
      await page.goto('httpy://www.whitehouse.gov'); // Intentionally bad protocol
      await browser.close();
    });
  } catch (error) {
    errorCaught = true;
    errorMessage = error instanceof Error ? error.message : String(error);
  }
  // Assert that an error was caught
  expect(errorCaught).toBe(true);
  // Optionally, check the error message contains expected content
  expect(errorMessage).toContain('net::ERR_ABORTED');
});

Why Use try/catch Instead of Custom Matchers?

Playwright doesn't provide a built-in matcher for error expectations like pytest.raises. However, try/catch blocks are fully supported and give you total control over the error message, how it's caught, and what behavior you want to assert afterward.

You can go beyond just checking that an error occurred-you can validate specific error types, compare error messages, and decide whether to re-throw or suppress the error.

Tips for Cleaner Code

If you find yourself doing this often, you can extract the logic into a helper function:

async function expectError(fn: () => Promise<void>, expectedMessagePart: string) {
  let errorCaught = false;
  let errorMessage = '';
  try {
    await fn();
  } catch (error) {
    errorCaught = true;
    errorMessage = error instanceof Error ? error.message : String(error);
  }
  expect(errorCaught).toBe(true);
  expect(errorMessage).toContain(expectedMessagePart);
}

And use it like this:

test('should throw an error for bad URL', async ({ playwright }) => {
  await expectError(async () => {
    const browser = await playwright.chromium.launch();
    const page = await browser.newPage();
    await page.goto('httpy://www.whitehouse.gov');
    await browser.close();
  }, 'net::ERR_ABORTED');
});

Final Thoughts

try/catch isn't just a fallback-it's a flexible and explicit way to perform negative testing in Playwright. It gives you visibility into what exactly went wrong and control over how to validate it. Whether you're validating error messages, failed network calls, or unhandled exceptions, try/catch should be part of your Playwright testing toolkit.

Permalink
April 3, 2025

In Automation Consistency Is Key

PlayWright Flaky Test Check

In Automation Consistency Is Key

Tests that pass reliably instill confidence in your codebase, while unpredictable ones can erode trust and waste valuable time. Enter flaky tests - those pesky tests that don't always pass on the first try, even when the underlying code seems fine.

Fortunately, Playwright, a powerful browser automation framework, offers a handy tool to help QA automation teams tackle this challenge head-on: the --fail-on-flaky-tests option.


What Are Flaky Tests?

Flaky tests are the wild cards of the testing world. They might pass on one run and fail on the next, often due to timing issues, external dependencies, or subtle environmental differences.

While they don't necessarily indicate a bug in the application, they do signal a problem in the test itself - something that might need a closer look to ensure reliability.

For QA teams, flaky tests can be a headache. They slow down workflows, muddy the waters of test reporting, and make it harder to trust the results of a test suite. That's where Playwright steps in with a feature designed to shine a spotlight on these troublemakers.


Playwright's Flaky Test Detection

Playwright provides a built-in mechanism to identify and flag flaky tests, helping teams address them proactively. By enabling certain configurations, Playwright can detect when a test exhibits inconsistent behavior - passing sometimes and failing others - and mark it as flaky.

This doesn't just sweep the issue under the rug; it gives teams actionable insights to refine their tests.

One particularly useful option in Playwright's arsenal is --fail-on-flaky-tests. By default, this option is set to false, meaning Playwright will note flaky tests but won't treat them as failures in the overall test run.

However, when you flip this switch to true, Playwright takes a stricter stance: if any test is flagged as flaky, the entire test suite fails. This forces the team to confront the issue immediately rather than letting it linger.


How to Use --fail-on-flaky-tests

To leverage this feature, simply add the --fail-on-flaky-tests flag when running your Playwright tests from the command line. For example:

            
                npx playwright test --fail-on-flaky-tests
            
        

Permalink
March 27, 2025

PlayWrite Date format

Typescript vs Python

Amazing how easier it is to get the date format for saving screenshots. Why is it so complicated in Typescript?

Here's getting the date in YYYY-MM-DD-HH-MM format:

Typescript


function getCurrentDateTimeFormatted(): string {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
  const day = String(now.getDate()).padStart(2, '0');
  const hours = String(now.getHours());
  const minutes = String(now.getMinutes());
  return `${year}-${month}-${day}-${hours}-${minutes}`;
}
// Example usage
const formattedDateTime = getCurrentDateTimeFormatted();

Python


from datetime import datetime
datefile = datetime.now().strftime("%Y-%m-%d-%H-%M")

Permalink
March 20, 2025

Parametrization in PlayWright

Test Uptime Status of Multiple Sites

Yesterday, I showed how to use Parametrization in Pytest. Here's an example of how you would run that same code in PlayWright with TypeScript:

This code checks to make sure the four websites are up and running. This is just a quick sanity test, it doesn't do any critical path testing.


import { test, expect } from '@playwright/test';
// List of websites to test
const WEBSITES = [
  "https://www.company.com",
  "https://qa1.company.com",
  "https://qa2.company.com",
  "https://stage.company.com",
];
// Configure Playwright to run in headless mode globally
test.use({ headless: true });
test(`Check Websites Status`, async ({ page }) => {
// Iterate over websites to create a test for each
    for (const website of WEBSITES) {
        test(`Check if ${website} is up and running`, async ({ page }) => {
            try {
                // Attempt to load the website
                await page.goto(website, { waitUntil: 'domcontentloaded' });
                // Check if page title exists and is not empty
                const title = await page.title();
                expect(title).not.toBe('');
                // Check if body element exists
                const body = page.locator('body');
                await expect(body).toBeVisible();
                // Log success
                console.log(`✓ ${website} is up and running (Title: ${title})`);
            } catch (error) {
                // Oh the Horror: Fail the test with a detailed message
                throw new Error(`Website ${website} failed: ${error.message}`);
            }
        });
    }
})

Permalink
March 13, 2025

Get Random Line from a File

Add some variety to your PlayWright tests

In PlayWright, you can easily get the contents of a file to include in a form. This is the getRandomLineFromFile function that I use to open up a local file and get a random line:


async function getRandomLineFromFile(filePath: string): Promise {
    try {
        // Read file content and split into lines
        const fileContent = await fs.readFile(filePath, 'utf8');
        const lines = fileContent.split('n').filter(line => line.trim() !== ''); // Remove empty lines
        
        if (lines.length === 0) {
            throw new Error('File is empty');
        }
        
        // Get random line
        const randomIndex = Math.floor(Math.random() * lines.length);
        return lines[randomIndex].trim();
    } catch (error) {
        console.error(`Error reading file: ${error}`);
        throw error;
    }
}

I would use this to open up a file that has a random sentence to fill in a feedback form. Here's an example PlayWright with TypeScript entry that I would use:


test('Random Feedback Form Test', async ({ page }) => {
    const filePath = '/Users/dict/srand.txt';
    const randomLine = await getRandomLineFromFile(filePath);
    await page.goto('https://www....');
    await page.waitForLoadState("networkidle");
    await page.getByLabel('name').fill('Chris Ryan');
    await page.getByLabel('comment').fill(testText);
....
})

You could also do this to randomize names, locations etc. This is just handy to have when you want to add some variety to a test run.

Permalink
March 6, 2025

XPath with Playwright page.locator

A Practical Guide

Playwright page.locator method is designed to find elements dynamically, with built-in support for multiple selector types - including CSS, text, and yes, XPath. While CSS selectors are great for straightforward queries, XPath shines when you need more flexibility or when dealing with complex DOM structures.

Here's why XPath might be your go-to:

  • Structural Navigation: XPath lets you traverse the DOM based on relationships (e.g., parent, sibling, child) rather than just classes or IDs.

  • Attribute Precision: Target elements by any attribute, not just class or id.

  • Text-Based Selection: Easily find elements containing specific text, even partial matches.

  • Dynamic Pages: XPath can handle scenarios where CSS selectors falter, like when class names are auto-generated or unpredictable.

Playwright's page.locator makes XPath a first-class citizen, so let's see it in action.

Getting Started with page.locator and XPath

The syntax for using XPath in page.locator is simple: prefix your XPath expression with xpath= or use the double-slash shorthand //. Here's the basic structure:


await page.locator('xpath=//tag[@attribute="value"]').click();

Playwright will evaluate the XPath expression and return a Locator object, which you can then interact with (e.g., click(), fill(), textContent()).

Practical Example

Let's walk through a real-world scenario where XPath and page.locator save the day.

Targeting an Element by Attribute

Imagine a login form with a button lacking a unique ID or class:


<button type="submit" data-test="login-btn">Sign In</button>

With XPath, you can target it by its data-test attribute:


const { test } = require('@playwright/test');
test('click login button', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.locator('xpath=//button[@data-test="login-btn"]').click();
});

The //button[@data-test="login-btn"] means "find any <button> element with a data-test attribute equal to login-btn."

When to Avoid XPath

While XPath is powerful, it’s not always the best choice:

  • Simple Selectors: Use CSS for #id or .class - it’s faster and more readable.
  • Dynamic IDs: If attributes change frequently, text-based or role-based selectors (role=) might be more stable.
  • Maintenance: Complex XPath expressions can become brittle if the DOM structure shifts.

Permalink
December 10, 2024

PlayWright URL Scraping

Sample Code to get all URLs

While experimenting with Playwright this week, I put together a script that grabs all the URLs from a website and writes them to a file. Here's the code that I finally came up with:

This approach is particularly useful when you need to ensure that all the anchor tags on the homepage are functioning as expected. By verifying the anchor tags separately, you can isolate any issues related to broken or misconfigured links, making it easier to pinpoint and address problems.

Additionally, I'll create another test specifically to validate that the URLs associated with these anchor tags are correct. This two-pronged strategy ensures that both the structure and the destinations of your links are accurate.

Pro Tip: The reason for separating these tasks, instead of validating the URLs while scraping the homepage, is to enhance the efficiency of your test execution. By dividing the workload into smaller, targeted tests, you can leverage parallel execution to speed up the overall testing process. This approach not only reduces the total runtime of your test suite but also provides clearer insights into potential issues, allowing you to debug faster and more effectively.

Permalink

About

Welcome to Playwright Tips and Tricks, your go-to resource for mastering the art of web automation and testing with Playwright! Whether you're a seasoned developer looking to streamline your workflows or a curious beginner eager to dive into the world of browser automation, this blog is designed with you in mind. Here, I'll share a treasure trove of practical insights, clever hacks, and step-by-step guides to help you harness the full power of Playwright - a modern, open-source tool that's revolutionizing how we interact with web applications.

Check out all the blog posts.

Blog Schedule

Friday 25 Macintosh
Saturday 26 Internet Tools
Sunday 27 Misc
Monday 28 Media
Tuesday 29 QA
Wednesday 30 Pytest
Thursday 1 PlayWright