Using Playwright Without a Test Runner — Alternatives to @playwright/test and Their Trade-offs

Using Playwright Without a Test Runner — Alternatives to @playwright/test and Their Trade-offs

Playwright has not only @playwright/test but also a core library called playwright that can be used without a test runner. For cases requiring RPA-style browser automation or a custom orchestration layer, I have organized the options that are not tied to a test framework along with their tradeoffs, based on concrete examples.
2026.08.01

This page has been translated by machine translation. View original

Introduction

When it comes to Playwright, the common approach is to use @playwright/test and write tests with test() and expect(). However, Playwright has a core library called playwright that can also be used as a browser automation script without a test runner.

This article introduces a real-world case where we adopted the plain playwright library instead of @playwright/test. We'll break down why we made that choice, what we gave up, and what we gained.

Prerequisites & Environment

  • playwright (core library)
  • TypeScript
  • Path alias configuration with module-alias

Differences Between @playwright/test and playwright

playwright              → Browser automation library (core)
@playwright/test        → Test runner + playwright core

@playwright/test is playwright with a test runner added on top. It includes the following features:

Feature @playwright/test playwright (core)
Browser automation
test() / expect() ×
Fixtures ×
Auto retry ×
Parallel execution ×
Reporter ×
Trace / Screenshot ○ (automatic) ○ (manual)
playwright.config.ts ×

Writing Test Scripts with Plain playwright

scripts/example.ts
import "module-alias/register";
import { chromium } from "playwright";
import CheckoutFlow from "@class/screen/CheckoutFlow";

(async () => {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext({ locale: "en-US" });
  const page = await context.newPage();

  await CheckoutFlow.new(page, "STAGING")
    .then((flow) => flow.addItemToCart({ productId: "A-001", quantity: 2 }))
    .then((flow) => flow.enterShippingAddress("東京都渋谷区1-2-3"))
    .then((flow) => flow.selectPaymentMethod("クレジットカード"));

  process.exit(0);
})();

(async () => { ... })() — wrapped in an immediately invoked async function expression (IIFE). In @playwright/test, test() plays this role, but with the plain library you need to write it yourself. Using module-alias, project classes are referenced with path aliases like @class/....

Why We Didn't Use @playwright/test

1. RPA-style scripts, not tests

The purpose of this project was not "pass/fail judgment of test results," but "automatically executing business flows." There were almost no situations requiring assertions with expect(), making the test() framework overkill.

2. Building a custom orchestration layer

import Browser from "@class/wrapper/Browser";
import Slack from "@class/wrapper/Slack";
import Spreadsheet from "@class/wrapper/Spreadsheet";

At the top of the script, we import custom wrapper classes. Integration with external services (such as Slack notifications and spreadsheet output) was required beyond just browser automation, and we didn't want to be constrained by @playwright/test's test lifecycle.

3. Complete control over execution flow

With @playwright/test:

  • The framework controls test execution order
  • Each test is assumed to be independent for parallel execution
  • State is managed through fixtures

With plain playwright:

  • Scripts execute sequentially from top to bottom
  • Results from previous steps can be used in subsequent steps
  • External API calls or file I/O can be inserted at any point

The Intent Behind headless: false

const browser = await chromium.launch({ headless: false });

headless: false displays the browser UI. During development, you can visually verify the operations, making debugging significantly easier.

In CI environments, switch to headless: true:

const browser = await chromium.launch({
  headless: process.env.CI === "true",
});

The Importance of locale Configuration

const context = await browser.newContext({ locale: "en-US" });

We explicitly set the locale. This affects:

  • Date picker display format (MM/DD/YYYY vs DD/MM/YYYY)
  • Numeric thousand separators (1,000 vs 1.000)
  • The browser's Accept-Language header

If the application under test references navigator.language to switch its display, not specifying a locale can cause results to vary between test environments.

A Note on process.exit(0)

process.exit(0);

We explicitly terminate the process at the end of the script. Without this:

  • The Node.js process won't exit while the browser's WebSocket connection remains open
  • Especially with headless: false, the browser window stays open

A cleaner cleanup approach:

try {
  // Test flow
  await CheckoutFlow.new(page, "STAGING")
    .then((flow) => flow.addItemToCart({ productId: "A-001", quantity: 2 }));
} finally {
  await browser.close();
}

Calling browser.close() closes the browser and the process exits naturally. With this approach, process.exit(0) is unnecessary. Since process.exit(0) skips finally blocks, it can cause resource leaks.

When to Consider Migrating to @playwright/test

Consider migrating to @playwright/test when the following requirements arise:

  • Test result reporting becomes necessary (HTML Reporter, JUnit Reporter)
  • You want to reduce test time with parallel execution in CI/CD
  • Automatic retry on failure is needed (in unstable network environments)
  • You want to manage common test setup with fixtures
  • The team grows and standardization of test structure becomes necessary

Conversely, if you have a small number of scripts and RPA-style usage is the primary purpose, plain playwright is sufficient.

Summary

  • playwright (core) can be used as a browser automation library without a test runner
  • By not using @playwright/test, you gain complete control over execution flow and freedom in external service integration
  • In return, retry, parallel execution, and reporting must either be implemented yourself or foregone
  • Develop with headless: false, lock display formats with locale, and clean up properly with browser.close()
  • Consider migrating to @playwright/test as the team size and number of tests grow

It's important not to be bound by the "correct way" to use a tool, but to choose the approach that fits your project's purpose.

Share this article

Related articles