Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use an array of records to declare one Playwright Test case per input, use projects when you want the same tests to run under different configurations, and use fixtures when test data needs reusable setup or cleanup. These patterns solve different problems; choosing the right one keeps cases understandable and independent.

Run one test for each data record

For a small, fixed set of related cases, store each input and its expected result in an array, then loop over the records to declare a test for each one. Playwright Test discovers the declared tests before running them. Give every case a useful, distinct title so the report identifies the failing input without requiring you to inspect the source.

The following example uses the Playwright Test API and a page fixture. It assumes the application has a greeting page at /greet, accepts a name query parameter, and displays the expected greeting as page text. Replace that route and assertion with behavior your application actually exposes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { test, expect } from '@playwright/test';

const greetingCases = [
  { name: 'Ada', expected: 'Hello, Ada!' },
  { name: 'Grace', expected: 'Hello, Grace!' },
  { name: 'Linus', expected: 'Hello, Linus!' },
];

test.describe('greeting page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/greet');
  });

  for (const { name, expected } of greetingCases) {
    test(`greets ${name}`, async ({ page }) => {
      await page.getByLabel('Name').fill(name);
      await page.getByRole('button', { name: 'Greet' }).click();
      await expect(page.getByText(expected, { exact: true })).toBeVisible();
    });
  }
});

Each loop iteration calls test(), so the report shows three distinct tests rather than one test internally checking three values. The shared beforeEach hook is outside the loop: it belongs to the describe block and runs for each test in that group. This arrangement follows the documented parameterization pattern. See the Playwright parameterize tests guide for the official example and alternatives using test.describe() or multiple declarations.

Make cases informative and genuinely independent

  • Include a distinguishing value in the title, such as a role, boundary, or input label. Avoid titles that differ only by an opaque index.
  • Keep each record self-contained: include the input and the expected user-visible outcome that belongs to that case.
  • Use values that exercise meaningful behavior, such as ordinary input, an empty value, a boundary, or a rejected value. Do not add cases just to inflate the count.
  • Do not let a prior test’s browser state or server-side changes determine what a later record observes. Give each case controlled setup and, when needed, a unique record or cleanup.

For example, if a form must reject an empty name, represent that expected outcome explicitly instead of assuming every record leads to a greeting. You can add a case property such as expectedError and branch inside the test, but if success and failure cases require substantially different setup or assertions, separate test declarations or helper functions may be easier to read.

Use projects for configuration variation

A data row answers “what input and outcome should this case use?” A project answers “under what configuration should this test suite run?” Projects can represent browsers, devices, environments, or custom option values. If the test behavior is unchanged but a setting changes across runs, configure projects rather than duplicating the data loop. The projects guide describes project configuration and dependencies, including setup and teardown projects.

For instance, suppose an application supports two account modes and the test reads a configurable option. Define the option fixture, then assign its value in named projects. The test itself receives the value as a fixture and can use it without hard-coding project-specific branches.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// playwright/fixtures.ts
import { test as base } from '@playwright/test';

export type TestOptions = {
  accountMode: 'standard' | 'enterprise';
};

export const test = base.extend<TestOptions>({
  accountMode: ['standard', { option: true }],
});
export { expect } from '@playwright/test';
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'standard account',
      use: { accountMode: 'standard' },
    },
    {
      name: 'enterprise account',
      use: { accountMode: 'enterprise' },
    },
  ],
});
// account.spec.ts
import { test, expect } from './playwright/fixtures';

test('shows the correct account mode', async ({ page, accountMode }) => {
  await page.goto('/account');
  await expect(page.getByTestId('account-mode')).toHaveText(accountMode);
});

Use the fixture import consistently for tests that need the custom option. Project settings are applied to the tests in that project; a project is a logical group, not a separate test implementation. The project configuration can also vary browser/device settings or other configuration such as timeouts and retries, but running a suite in more configurations means more test executions and potentially more runtime.

Use fixtures when data has a lifecycle

A static array is usually the clearest home for a few immutable input-and-expected-result pairs. A fixture is more appropriate when tests need a resource prepared or reset in a consistent way—for example, creating a test account, obtaining a configured page, or arranging reusable test data. Fixtures are on-demand and composable, and Playwright manages their isolation and lifecycle according to their scope. Read the fixtures guide before choosing scope or adding teardown.

Keep the data source separate from the lifecycle decision. Playwright supports test parameterization and fixtures, but those capabilities do not mean that a spreadsheet, CSV file, or external data provider is automatically a built-in data source. If you load data from elsewhere, your code or supporting library must provide that loading behavior. For many tests, a typed local array is simpler, easier to review, and more reliable than introducing an external source.

Scope and state need to match

  • Use per-test setup for mutable state that one case could alter. A shared mutable account or database row can cause order-dependent failures.
  • Use broader-scoped resources only when sharing is safe, and ensure concurrent tests cannot corrupt one another’s state.
  • Give created server-side data a cleanup plan. If cleanup is not reliable, use unique identifiers or isolated test environments so abandoned records do not affect later runs.
  • Prefer a fixture when the same setup and teardown are genuinely reused; do not create a fixture merely to hold a small constant array.

Choose the pattern that matches the variation

What varies Pattern Best fit Watch for
Inputs and expected outcomes for one behavior Array of records, one declared test per record Small, related cases with distinct report names Duplicate or unclear test titles; shared state between cases
Browser, device, environment, or option values Projects, optionally with option fixtures Running the same test logic under different configurations Extra executions and configuration-specific behavior
Reusable setup, resources, and cleanup Fixtures Lifecycle-managed data or resources needed by tests Incorrect scope, teardown gaps, accidental shared mutable state

These techniques can be combined. For example, each project can run a data-driven set of cases, while a fixture creates isolated records. Combine them only when they represent real independent dimensions: multiplying cases by projects increases the number of executions and can make reports harder to navigate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep data-driven tests reliable

Data-driven tests are still tests: a large or clever table cannot compensate for uncontrolled state or assertions that only verify implementation details. Playwright’s best practices recommend isolated tests and assertions on behavior users can observe. Apply that guidance to every case.

  • Assert the visible result or other user-observable behavior rather than private implementation state.
  • Control relevant storage, cookies, and application data for each case. Do not rely on the preceding row to prepare the next one.
  • Use unique test records when cases write to a shared service, and clean them up where appropriate.
  • Run cases independently during development. A test that passes only after another case has run is hiding a dependency.

Scheduling is not a substitute for isolation. Playwright’s parallelism documentation says tests in one file run in order by default, while files run in parallel. That default does not make it safe to depend on order: execution configuration can change, and parallel files can contend for shared external resources. Design each case so it can stand on its own.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Run and diagnose the cases

Once tests are declared, run them with the project’s usual Playwright Test command. For example, if the project has the standard test script configured, use npx playwright test; to narrow execution, pass a test file path or use the runner’s project selection options documented for your installed version. Check the report’s distinct case title first, then inspect the data record, setup, and assertion associated with that case.

Common problems and fixes

  • Several rows appear as one test. The loop may be inside a single test body. Declare a separate test() for each record at test-definition time.
  • Duplicate-title errors or ambiguous reports. Include a stable, distinguishing value in each title; do not use the same generic title for every iteration.
  • A case reads the wrong expected value. Put input and expected output on the same record and destructure that record in the loop. Avoid indexing into a separate array whose order can drift.
  • Tests pass alone but fail in the suite. Look for shared cookies, browser storage, server records, or order-dependent setup. Reset or isolate state rather than rearranging tests.
  • Failures occur only with project configuration. Confirm the test imports the custom fixture, the option is declared with the correct type, and each project’s use value matches the option’s expected type.
  • Adding a data source makes runs inconsistent. Check that loading completes before declaring tests and that the source is deterministic and available in the test environment. Playwright’s parameterization pattern does not itself supply a CSV or spreadsheet loader.
  • Many cases make the suite slow or reports unwieldy. Keep only meaningful cases, split unrelated behaviors into separate tests, and avoid multiplying every case across projects unless that cross-product is needed.

Or skip the browser setup

Playwright is for browser tests; ScreenshotNeo is a separate screenshot API, not a Playwright test runner. If your task is to capture a page image rather than assert application behavior in a browser test, one GET request can return a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for request options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, with response headers indicating the page verdict and billing status. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. ScreenshotNeo is made by Yorker Media; visit ScreenshotNeo for product details, or sign up free for 1,000 screenshots a month with no card.

Frequently Asked Questions

Can a Playwright test use data loaded from a CSV file?

Playwright’s parameterization pattern does not provide a built-in CSV or spreadsheet data provider; loading and supplying such data is the test code’s responsibility.

Should I use a loop or projects for multiple browsers?

Use projects for browser or device configuration variation, and a record loop for case-specific inputs and expected outcomes.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.