Skip to content

Authentication

If your app requires login, run /shiplight auth and describe your login flow — your agent sets up (or repairs) the login and saved storage state using one of the patterns below. The patterns are documented here so you can review and adjust what it writes.

There are two approaches depending on whether all tests share the same account or individual tests need their own identity.

Shared Account (Most Common)

All tests share one authenticated session. This is the standard Playwright authentication pattern — a setup project logs in once, saves the browser state, and all tests reuse it.

ts
// auth.setup.ts
import { test as setup } from "@playwright/test";

setup("login", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill(process.env.USERNAME!);
  await page.getByLabel("Password").fill(process.env.PASSWORD!);
  await page.getByRole("button", { name: "Sign in" }).click();
  await page.waitForURL("/dashboard");
  await page.context().storageState({ path: ".auth/default.json" });
});
ts
// playwright.config.ts
export default defineConfig({
  ...shiplightConfig(),
  projects: [
    { name: "auth", testMatch: "auth.setup.ts" },
    {
      name: "default",
      dependencies: ["auth"],
      use: {
        baseURL: "https://staging.example.com",
        storageState: ".auth/default.json",
      },
    },
  ],
});

Tests don't need any account field — they're already authenticated via storageState. This is pure Playwright, no Shiplight-specific logic.

Multiple pre-defined accounts: If you have a small number of accounts (e.g., admin, viewer) but tests don't pick which one to use, parameterize auth.setup.ts with an environment variable and select the account at runtime:

bash
TEST_ACCOUNT=admin npx shiplight test

This is still a shared account — just selected at runtime instead of hardcoded.

Per-Test Auth (Advanced)

When individual tests need their own identity (e.g., one test runs as admin, another as viewer in the same run), each test declares its auth script and optional args inline. This allows login customization per test.

1. Auth login function

Create an auth.login.ts that exports a login(args) function. It receives the args from the test config, performs the login flow, manages storageState caching and expiration, and returns the path to a storageState JSON file.

ts
// auth.login.ts
import { chromium } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";

export async function login(args: Record<string, unknown>): Promise<string> {
  const stateFile = path.join(".auth", `${args.username}.json`);

  // Return cached state if it exists (you control expiration logic here)
  if (fs.existsSync(stateFile)) return stateFile;

  // Perform login
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto("/login");
  await page.getByLabel("Email").fill(args.username as string);
  await page.getByLabel("Password").fill(args.password as string);
  await page.getByRole("button", { name: "Sign in" }).click();
  await page.waitForURL("/dashboard");

  // Cache storageState (cookies + localStorage + IndexedDB)
  fs.mkdirSync(path.dirname(stateFile), { recursive: true });
  await context.storageState({ path: stateFile, indexedDB: true });
  await browser.close();

  return stateFile;
}

2. Use in tests

Each test declares auth (path to the login script) and optional args passed to the login function:

yaml
use:
  auth: ./auth.login.ts
  args:
    username: admin@example.com
    password: "{{ADMIN_PASSWORD}}"
goal: Admin can manage users
statements:
  - URL: /admin/users
  - VERIFY: User management page is visible

The args object is passed directly to the login function. This means your auth script can receive any fields your login flow needs (TOTP secrets, API tokens, org IDs, etc.). Tests that don't need args can omit the args field entirely.

The fixture uses the storage state path returned by the login() function to create the browser context, so the test runs in an already-logged-in state.

Tests without auth use the default context — if a setup project is configured, they get its storageState; otherwise they run unauthenticated.

Released under the MIT License.