Home / Services / Playwright test automation

Playwright test automation that covers the flows you cannot ship without

You name the journeys that must never break. They come back as end-to-end tests that run on every commit, and as a document that tells your team how to add the next one.

Short answer

Firm86 is a Playwright testing company. We build suites for teams that have none, or that started one and stalled: the flows ranked, the fixtures and test data they run on, and a CI job that runs them on every commit. Engineers are billed hourly, from $50 an hour, minimum one full-time engineer for one month.

Expect questions back first: which journeys, how many environments, and whether there is anything in a tests/ folder already.

Who this is for

Two teams read this page. One ships a web application with unit tests and a release that waits on somebody clicking through checkout. The other started end-to-end tests eight months ago and the enthusiast who started them has moved on. Neither is shopping for a framework, and neither can picture the finished thing: how many tests, over which journeys, where the data comes from, and who owns it in June.

  • No end-to-end tests at all, and a deploy that waits on somebody being free to click.
  • A tests/ folder with sixty specs, no fixtures/ directory, and a sign-in through the form at the top of every file.
  • page.waitForTimeout(3000) in nine places, each one added in a week when something had to ship.
  • A pipeline that runs the unit tests and has never run these ones.

Playwright ships four official bindings — TypeScript and JavaScript, Python, Java and .NET — and we deliver a suite in all four, so the one you get is in the language your team already reads.

Playwright's own test runner ships only with the JavaScript and TypeScript binding: the playwright.config.ts file, projects, fixtures, retries, sharding and the HTML reporter all belong to it. A Python suite runs under pytest, a Java one under JUnit or TestNG, a .NET one under MSTest, NUnit or xUnit. The specs and the traces are the same either way, and the pipeline described below takes its shape from whichever runner sits underneath it.

If you already have a suite that runs — Selenium, Cypress, Protractor or anything else driving a browser — the job is a conversion and it starts at Playwright migration services. Where a Playwright suite already covers those journeys and nobody trusts its red builds, coverage is not the problem and the engagement is flaky suite repair. This engagement delivers a suite covering your flows; the harness underneath comes with it and is not what is being sold, so a team that wants the harness alone should say so in the first email.

Four people shipping an internal tool once a month do not need any of this; a checklist will serve them for years. The spend makes sense where a bad deploy reaches customers before anybody in the building notices.

What you get

The suite, in your repository

Specs covering the journeys you ranked, with the fixtures and the test-data setup they depend on, arriving through your own pull requests. Every spec is written against the conventions the first flow settled, so the tenth file looks like the first.

The pipeline definition, and what a failure leaves behind

The job that runs the suite on every commit, split across machines, with the reporter and the trace wired up. A red run comes back as a trace a developer opens and walks through action by action, not as a red tick and a log somebody has to reconstruct.

A runbook for the team that inherits it

How to add a test for a new journey, which locator to reach for first and when to reach for a test id instead, how to read a failure, and how to update a screenshot baseline. It is the document that decides whether the suite is still running a year later.

How it works

  1. The flow list

    We ask for the journeys that must never break, in order. If there is no such list, producing one is the first deliverable, and it is a conversation with whoever answers for revenue rather than with your engineers — the ranking is a commercial judgement and it decides everything after it. You give us an hour of that person's time, read access to the repository, and an environment we can point a browser at.

  2. A walking skeleton

    One flow, end to end, green in your pipeline, before a second test is written. Everything we would otherwise argue about in a document — the directory layout, the fixture, the locator policy, where test data comes from — gets argued over one running test instead, while changing our minds is still cheap. You give us review time from somebody who knows the product.

  3. The build-out

    The rest of the ranked list, in pull requests small enough to read. Each one merges and starts running in your pipeline before the next is opened, so stopping the engagement at any point leaves you owning the tests that have merged, and there is never a long-lived branch holding the suite. Test data sets the pace here: a flow whose starting state can be created over an API is cheap to automate, and the same flow reached only by clicking through four screens is not.

  4. Handover, against the runbook

    One of your engineers writes a test for a journey we did not cover, working from the runbook, while we are still there. Anything they get stuck on is a gap in the runbook, and it gets fixed before we go.

None of those phases carries a date. The first one is where the length gets measured, against your flow list and your own test data, and until it has been we would rather say nothing than publish a number you would hold us to.

The code we would write for you

Owning a suite two years in comes down to how the repository is laid out, how a test gets a logged-in user with its own data, and how an element is named. None of those is a Playwright feature. They are choices, and you can disagree with ours before you buy. The tree we hand over:

playwright.config.ts
package.json
tests/
  checkout/
    guest-checkout.spec.ts
    discount-code.spec.ts
  signup/
    email-signup.spec.ts
  billing/
    refund-an-order.spec.ts
fixtures/
  index.ts             # the test object every spec imports
  customer.ts          # makes a customer over the API, removes it afterwards
  seed.ts              # the only module that talks to your test-data endpoints
.github/workflows/e2e.yml    # or the equivalent file for your CI
Every sample on this page is written against Playwright 1.62.

npm init playwright@latest produces three of those lines — a config, a package.json and a tests/ directory with one example spec, as the installation documentation shows. Everything else is ours, and two decisions in it are worth arguing about. tests/ is organised by journey and not by page, because the question you get asked in a meeting is whether checkout is covered and a page-shaped tree cannot answer that; page objects still exist, as plain classes beside the flow that uses them. And seed.ts is the only module allowed to create data, because in every suite with three places that make a customer, the third was written by somebody who did not know about the first two.

The fixture is where a suite becomes able to run in parallel, or does not:

// fixtures/index.ts
import { randomUUID } from 'node:crypto';
import { test as base, expect, type Page } from '@playwright/test';

type Customer = {
  id: string;
  email: string;
  password: string;
};

export const test = base.extend<{ customer: Customer; customerPage: Page }>({
  customer: async ({ request }, use) => {
    const email = `e2e-${randomUUID()}@example.test`;
    const password = 'hunter2';

    const created = await request.post('/api/test-support/customers', {
      data: { email, password, plan: 'pro' },
    });
    expect(created.ok()).toBeTruthy();
    const { id } = await created.json();

    await use({ id, email, password });

    await request.delete(`/api/test-support/customers/${id}`);
  },

  customerPage: async ({ page, customer }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill(customer.email);
    await page.getByLabel('Password').fill(customer.password);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page.getByRole('link', { name: 'Account' })).toBeVisible();

    await use(page);
  },
});

export { expect };

A spec asks for customerPage in its argument list and gets a browser signed in as a customer nobody else is using. The line after await use(...) is what keeps that true at the end of a run as well as the start: the fixtures documentation puts setup before that call and teardown after it, so the account is removed in the same block that created it, where a beforeEach would have let the cleanup drift into an afterEach somewhere else. Worker scope is the tempting change here and it has a price. { scope: 'worker' } creates the customer once per worker process instead of once per test, which is faster, and every test in that worker then shares one account and can see what the last one did to it. We use it for state that gets read and never written.

Then the argument you will have with your own team, about naming elements:

// tests/checkout/discount-code.spec.ts
import { test, expect } from '../../fixtures';

test('applies a discount code to the order', async ({ customerPage: page }) => {
  await page.goto('/checkout');

  // Located by what a customer reads. If this label changes, the test should
  // notice, because a customer would have noticed too.
  await page.getByRole('button', { name: 'Add a discount code' }).click();

  // A field from the design system. Its label is wired through a generated id
  // and moves when the component is upgraded, so it gets a contract instead.
  await page.getByTestId('discount-code').fill('SPRING26');

  await page.getByRole('button', { name: 'Apply' }).click();
  await expect(page.getByTestId('discount-applied')).toContainText('SPRING26');
});

Playwright's best practices documentation settles the default: "Prefer user-facing attributes to XPath or CSS selectors", because "Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests." It does not settle when to stop, and our answer there is a policy you can argue with. We reach for a data-testid where the accessible name is generated, where the text is translated, and where the element belongs to a component library nobody on your side controls. Adding one changes your application, so it arrives in the same pull request as the test, and a team that will not take those pull requests should say so before we start. The same page is blunt that you should "Only test what you control"; where a flow crosses into somebody else's server, the suite stubs the request at the network layer.

Visual, component and accessibility coverage

These live inside a suite: what we would add, when, and where each one stops helping.

Visual regression

A screenshot check pays where the bug is visual and no assertion would have caught it: a design system, a pricing page. A baseline is only worth anything if it was made in the environment the comparison runs in, which is most of what Playwright visual regression testing deals with.

Component testing

Component tests come in where a design-system team wants a component's states pinned down without driving the whole application to reach them, which is what Playwright component testing covers; the limit holds either way, because a component test does not cover a journey and we will not sell it as one.

Accessibility

An accessibility check goes into a suite when there is a commitment behind it: a contract clause, a public-sector customer, a WCAG level somebody has promised in writing. What that integration is and where it stops is Playwright accessibility testing, which keeps the same limit this page has always stated — a green axe run is not an accessible product, and nothing we hand you will say that it is.

Where this stops

A team buying end-to-end tests for the first time arrives with a list, and that list was written before anybody chose a framework. These are the items on it we hand straight back, each with what the suite covers at the same boundary.

  • Native mobile apps. Not offered here, and not something the framework does: Playwright drives browsers, and a native iOS or Android build is not one. The suite covers your mobile web at that boundary. devices['iPhone 13'] in the config gives a project a phone's viewport, user agent and touch input, and that emulation is a real deliverable for a responsive application. It does not reach the application somebody installed from a store.
  • Load and performance at scale. At this boundary the suite gives you a functional check on every commit, which is a different question from how the system behaves with a thousand people in it at once. Playwright measures one real browser doing one thing well; it is not k6, JMeter or Gatling, and no engagement sold here includes load testing.
  • Security and penetration testing. Not offered here, in any framework.
  • Safari on a real iOS device. The suite runs WebKit as a third project beside Chromium and Firefox, on every commit, which catches a large share of what a Safari user meets. It is not Safari on an iPhone: the browser documentation says Playwright does not work with the branded version of Safari, because it relies on patches. That version on that hardware needs a device cloud, and we do not sell one.
  • Internet Explorer. The framework does not support it, and neither do we.

What it costs

Nothing here is sold as a package or by the test. Engineers are billed hourly, from $50 an hour, depending on where the engineer sits. The minimum engagement is one full-time engineer for one month.

The estimate starts at your flow list: how many journeys are on it, how many environments and browser projects each has to run in, and whether there is a pipeline to add a job to. Test data stretches an estimate further than the flow count does. A product where a customer, a subscription and three months of invoices can be created with one API call is a different engagement from one where the only route to that state is through the screens, and the same twelve flows are priced differently in the two.

Read first

Questions

How long until we have tests running in our CI?

One flow is green in your pipeline before the second test is written, and that is the part of the schedule we are confident about. We publish no date for the whole suite, because the length is set by how many flows are on your list, how much of their test data can be created over an API instead of clicked into existence, how many environments and browser projects the suite runs in, and whether there is a pipeline to add a job to at all.

We already have some Playwright tests. Do you throw them away?

No. They get read first, and most of what is there is a flow somebody already understood well enough to automate, which is worth more than the code around it. A spec covering a flow on your list is rewritten onto the fixtures rather than deleted, and one covering a flow nobody ranked is left alone and named in the runbook. Where the suite already covers your flows and the problem is that nobody believes its red builds, the engagement you want is flaky suite repair.

Can you write the suite in Python or .NET instead of TypeScript?

Yes. Playwright ships four official bindings — TypeScript and JavaScript, Python, Java and .NET — and we deliver a suite in all four, so a Django shop gets Python and a .NET shop gets .NET. The language decides the runner underneath. Playwright's own test runner ships only with the JavaScript and TypeScript binding, so a Python suite is built on pytest and a .NET suite on MSTest, NUnit or xUnit, and the config-driven projects, retries, sharding and HTML reporter described above have no direct equivalent there. Everything shown above is TypeScript, which is this site's default for samples and says nothing about the language your suite arrives in. Nobody has measured whether the lead time differs by language, so nobody here will tell you it does not.

Can you test our iOS app?

No. Playwright drives browsers. It can emulate a mobile browser, which covers a responsive web app on a phone-sized viewport, and it cannot drive a native iOS or Android application. If that is what you need, we are the wrong company and we will say so on the first call. The mobile-web half is on the table: the same flows, run in a project configured with a phone's viewport, user agent and touch input, next to the desktop ones.

Who maintains the suite after you hand it over?

Your team, using the runbook, and the handover phase exists to prove that is possible before anybody leaves: one of your engineers writes a test for a flow we did not cover, out of the runbook, while we are still there to watch it go wrong. Teams who would rather not carry it in-house hire Playwright engineers by the month instead, which is a different arrangement with its own page.

Do we need the audit before you start?

No. Most clients arrive knowing the job, and we scope that and start. The audit is for the team that wants to know what it has before it picks a direction, so it is a route in and never a gate. It is optional, it is billed by the hour, and how long it takes depends on the project. The minimum of one full-time engineer for one month does not apply to it.

Which journeys must never break?

Send the list, in order, even if it is five lines in an email, with a URL we can point a browser at. That is enough to come back with the flow we would build first as a walking skeleton and what the rest of the list looks like as work. We do not need an audit to start. If the answer to who owns the suite six months from now is somebody you have not hired, engineers by the month is the other thing we sell.