# Playwright jobs

How to write BernOps-compatible Playwright specs with clear runtime checks and reports.

BernOps executes Playwright test files. A saved job is still a normal Playwright spec; the difference is that BernOps stores it, dispatches it to a worker, injects project runtime values, and records telemetry.

## Required shape

At minimum, a job imports `test` from `@playwright/test` and declares one or more tests.

```ts
import { test, expect } from '@playwright/test';

test('checks the sign-in page', async ({ page }) => {
  await page.goto('/sign-in');
  await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
});
```

The generated Playwright config sets `use.baseURL` to the resolved BernOps base URL. That means `page.goto('/sign-in')` resolves against the run's base URL, a `BASE_URL` or `TARGET_URL` runtime variable, or `http://localhost:3000`.

## Prefer explicit runtime checks

If a job depends on a runtime value, fail early with a helpful message.

```ts
function requiredEnv(name: string) {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing required runtime variable: ${name}`);
  return value;
}

const API_KEY = requiredEnv('HAPPY_PATH_QUEUE_API_KEY');
```

This is better than letting a request fail later with an authorization error that hides the real issue.

## Node process versus browser page

Playwright tests run in Node. `process.env` exists in the test process, not inside the browser page. If a browser script needs a value, pass it deliberately.

```ts
const token = requiredEnv('TEST_SESSION_TOKEN');

await page.evaluate((value) => {
  window.localStorage.setItem('session_token', value);
}, token);
```

**Do not log secrets**

The worker redacts known secret values from logs and results, but jobs should still avoid printing tokens, passwords, API keys, cookies, or headers.


## Use `test.step` for readable reports

BernOps records Playwright steps and stdout. Structured steps make run detail pages easier to read.

For SDK-generated logs, nested steps, metrics, progress, and durable attachments without an API key, see the [Automation SDK native guide](/docs/automation-sdk#native-quick-start).

```ts
test('creates a queue item', async () => {
  await test.step('resolve backend and queue', async () => {
    // Fetch project resources and validate assumptions.
  });

  await test.step('add item', async () => {
    // Call the queue API.
  });
});
```

## Service calls from jobs

Jobs can call APIs directly with `fetch`. This is useful for setup, teardown, queues, and assertions that are easier outside the browser.

```ts
async function api<T>(baseUrl: string, token: string, path: string) {
  const response = await fetch(`${baseUrl}${path}`, {
    headers: { authorization: `Bearer ${token}` },
  });

  if (!response.ok) {
    throw new Error(`GET ${path} failed with ${response.status}: ${await response.text()}`);
  }

  return (await response.json()) as T;
}
```

## Timeouts and long-running flows

The default generated test timeout is 30 seconds. Long demos or polling flows should set an explicit timeout inside the test.

```ts
test('slowly populate a queue', async () => {
  test.setTimeout(180_000);
  // Long-running flow.
});
```

## Authoring checklist

| Concern | Recommendation |
| --- | --- |
| Runtime values | Read with `process.env` and validate at module load or test start. |
| Browser values | Pass values with fixtures, `page.evaluate`, request headers, or app UI inputs. |
| Secrets | Never print raw secret values. |
| Diagnostics | Use `test.step`, concise `console.log`, screenshots, and clear assertion messages. |
| Base URL | Prefer relative `page.goto()` paths unless the test intentionally targets another host. |
