This episode dissects the Playwright architecture: the roles of Browser, BrowserContext, and Page, the difference between the test runner and the standalone API, the test lifecycle from launch to close, as well as the project structure and basic configuration that form the foundation of the entire series.

Episode 1 explained why Playwright is the choice. Now it's time to understand how it works. This episode 2 dissects the Playwright architecture from the smallest components to the project structure, so you know exactly what happens behind the scenes when a test runs.
Understanding the architecture isn't just theory. When you later face strange bugs — like state leaking between tests, or contexts accidentally swapped — the answer is almost always at the architecture level: who owns the browser, which context is being used, and how each component's lifecycle works. Let's build that foundation now.
Browser is the topmost level: an instance of Chromium, Firefox, or WebKit launched from chromium.launch(). One browser instance can open many contexts. Launching a browser is a heavy operation, so by default Playwright runs a browser per test file, not per test.
BrowserContext is an isolated container inside the browser. Each context has its own storage session, cookies, localStorage, and service workers, completely separate from other contexts. This is the key to isolation in Playwright: two contexts in the same browser cannot see each other's data. We'll leverage this isolation for parallelism and security in episode 13.
Page is a tab or web page inside a context. Every action — click, form fill, navigation — operates at the page level. A context can open many pages, for example to handle popups or multiple tabs. The hierarchy flow is:
Browser
└── BrowserContext
└── Page
└── DOM + Network + JSBeyond web browsers, Playwright can also control Electron apps via the _electron.launch() class. This is useful for testing desktop applications built with Electron — apps that use Chromium as their runtime. This support lets Playwright serve not only the web, but also hybrid desktop applications.
Since version 1.0, Playwright ships a built-in test runner from the @playwright/test package. This runner provides fixtures like test, expect, page, and request, plus parallelism, retries, reports, and CI integrations. Most of this series uses the test runner.
Playwright can also be used as a standalone library via the playwright package without a test runner — ideal for automation scripts, scraping, or non-test tooling. Both modes use the same core API, but the test runner adds an orchestration layer.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
})();Note the order browser.launch() → browser.newContext() → context.newPage() → browser.close(). This sequence is the test lifecycle we'll discuss next.
In the test runner, the lifecycle is automated through fixtures. When a test asks for page, the runner launches a browser, creates a new context, opens a new page, runs the test, then cleans everything up when done — including closing the context and browser.
The full sequence:
For special scenarios, you can control the lifecycle explicitly using the browser and context fixtures:
import { test, expect } from '@playwright/test';
test('manual context', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
await context.close();
});Using browser.newContext() inside a test gives you full control — including creating several contexts at once or closing a context early. The test runner defaults remain the safest choice for most cases.
A tidy Playwright project usually looks like this:
playwright-project
├── tests/
│ ├── login.spec.ts
│ └── checkout.spec.ts
├── playwright.config.ts
├── package.json
└── .gitignoreTest files live in the tests directory, and the playwright.config.ts configuration file sits at the root. The .spec.ts and .spec.js extensions are recognized automatically by the runner.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});testDir determines where tests live, timeout sets the per-test time limit, and use.baseURL serves as the base URL for page.goto(). Full configuration details, including retries and webServer, will be covered in episode 3.
Episode 2 gave you the architectural foundation: the Browser → BrowserContext → Page hierarchy that forms the mental model of every Playwright operation, Electron support for desktop apps, the difference between the test runner and the standalone API, the test lifecycle from launch to close, as well as project structure and basic configuration.
Key takeaways:
playwright.config.ts controls testDir, timeout, and browser projects.In the next episode we'll discuss installation and basic configuration — installing Playwright and browser dependencies, setting up a complete playwright.config.ts, running your first test through the CLI, and managing timeouts and retries so your test suite is stable from the start.