Documentation
Documentation/Execution

Playwright jobs

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

playwrightjobstest.stepprocess.env
View Markdown
On this page

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.

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.

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.

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.

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.

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.

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

Authoring checklist

ConcernRecommendation
Runtime valuesRead with process.env and validate at module load or test start.
Browser valuesPass values with fixtures, page.evaluate, request headers, or app UI inputs.
SecretsNever print raw secret values.
DiagnosticsUse test.step, concise console.log, screenshots, and clear assertion messages.
Base URLPrefer relative page.goto() paths unless the test intentionally targets another host.