A focused toolkit for solving real-world Playwright E2E pain points without changing your existing workflow.
This repo is a small monorepo of production-ready utilities built on top of Playwright (not a testing framework).
@playwright-kit/auth- manage PlaywrightstorageStateartifacts before tests (multi-profile,ensure, CI-friendly failures).- Docs:
packages/auth/README.md - Install:
npm i -D @playwright-kit/auth - npm:
https://www.npmjs.com/package/@playwright-kit/auth - Examples:
examples/next-admin-auth,examples/vite-react-auth
- Docs:
See RELEASING.md.
Infrastructure utilities for managing Playwright storageState auth artifacts before running tests.
- Users define project-specific auth flows in
playwright.auth.config.ts - CLI generates
.auth/<profile>.json - CLI
ensurevalidates and refreshes states before tests (no auto-login during test execution)
Playwright gives you the low-level primitives:
- Save:
context.storageState({ path }) - Use:
test.use({ storageState: "state.json" }) - Optional: write a custom
globalSetupto generate states before a run
This package provides the missing "infrastructure layer" many teams end up building themselves:
- Multi-profile auth states (
admin,user,...) in one config file - Ensure workflow: validate existing states and refresh only when missing/invalid
- Explicit pre-test step (runs before
playwright test, not hidden inside fixtures) - Failure artifacts (trace + screenshot) in deterministic locations for CI debugging
- Optional app startup via
webServersoensurecan be self-contained in CI - Optional
.envloading via--dotenvto keep scripts copy/paste friendly
The "Native Playwright" section below is intentional: it shows that after generation, auth states are plain Playwright storageState files - there's no runtime magic.
npm i -D @playwright-kit/authWith pnpm:
pnpm add -D @playwright-kit/auth- Add
playwright.auth.config.ts(example below), then:
playwright-kit auth ensure --dotenv
playwright test- In tests, either use native Playwright:
test.use({ storageState: ".auth/admin.json" });or the wrapper:
import { authTest } from "@playwright-kit/auth";
export const test = authTest({ defaultProfile: "user" });
test.use({ auth: "admin" });Choose:
- Native: simplest; you reference
.auth/<profile>.jsondirectly. - Wrapper: cleaner multi-profile switching via
test.use({ auth: "<profile>" }).
- Config API:
defineAuthConfig(...) - CLI:
playwright-kit auth setup/playwright-kit auth ensure - Test ergonomics:
authTest(...)(optional)
This example supports two profiles: admin and user.
import { defineAuthConfig } from "@playwright-kit/auth";
export default defineAuthConfig({
baseURL: process.env.BASE_URL ?? "http://127.0.0.1:3000",
// Optional: have the CLI start your app and wait until it's reachable.
// Use this in CI (or anytime your app isn't already running).
// If omitted, the CLI assumes your app is already running at `baseURL`.
webServer: {
command: "npm run dev",
// Optional; defaults to baseURL when omitted.
// url: "http://127.0.0.1:3000/login",
},
profiles: {
admin: {
// Recommended: set validateUrl explicitly for deterministic validation.
// When omitted, the CLI will fall back to "/".
validateUrl: "/admin",
async login(page, { credentials }) {
await page.goto("/login");
await page.getByLabel("Email").fill(credentials.email);
await page.getByLabel("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
},
async validate(page) {
const ok = await page.getByRole("heading", { name: "Admin" }).isVisible();
return ok ? { ok: true } : { ok: false, reason: "Admin heading not visible" };
},
},
user: {
// Recommended: set validateUrl explicitly for deterministic validation.
// When omitted, the CLI will fall back to "/".
validateUrl: "/me",
async login(page, { credentials }) {
await page.goto("/login");
await page.getByLabel("Email").fill(credentials.email);
await page.getByLabel("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
},
async validate(page) {
const ok = await page.getByRole("heading", { name: "Me" }).isVisible();
return ok ? { ok: true } : { ok: false, reason: "Me heading not visible" };
},
},
},
});Default mapping is:
AUTH_<PROFILE>_EMAILAUTH_<PROFILE>_PASSWORD
Where <PROFILE> is uppercased and non-alphanumerics become _ (qa-admin -> QA_ADMIN).
Examples:
AUTH_ADMIN_EMAIL,AUTH_ADMIN_PASSWORDAUTH_USER_EMAIL,AUTH_USER_PASSWORD
This keeps tests "pure": auth refresh happens before Playwright tests run.
{
"scripts": {
"auth:ensure": "playwright-kit auth ensure --dotenv",
"pretest": "npm run auth:ensure",
"test": "playwright test"
}
}With pnpm:
{
"scripts": {
"auth:ensure": "playwright-kit auth ensure --dotenv",
"pretest": "pnpm run auth:ensure",
"test": "playwright test"
}
}Now:
npm testGenerate a single profile:
playwright-kit auth setup --profile admin --dotenvEnsure all profiles from config (default):
playwright-kit auth ensure --dotenvEnsure a subset:
playwright-kit auth ensure --profile admin --profile user --dotenvNode does not load .env automatically. If you want .env support:
playwright-kit auth ensure --dotenvOr:
playwright-kit auth ensure --dotenv-path .env.ciNotes:
.envloading is provided via thedotenvpackage.
Use this if you prefer the simplest, framework-native approach: you reference the generated storageState file directly.
This does not require any @playwright-kit/auth test wrapper.
import { test } from "@playwright/test";
test.use({ storageState: ".auth/admin.json" });Use this if you have multiple profiles and want clean switching via test.use({ auth: "user" }) (Playwright-native patterns),
while still keeping auth refresh outside of test execution.
Create a single fixtures file and reuse it:
// tests/fixtures.ts
import { authTest } from "@playwright-kit/auth";
export const test = authTest({ defaultProfile: "user" });
export const expect = test.expect;Usage (default profile):
import { test, expect } from "./fixtures";
test("user by default", async ({ page }) => {
await page.goto("/me");
await expect(page.getByTestId("whoami")).toHaveText("user");
});Switch profile using native Playwright patterns:
import { test, expect } from "./fixtures";
test.use({ auth: "admin" });
test("admin view", async ({ page }) => {
await page.goto("/admin");
await expect(page.getByTestId("whoami")).toHaveText("admin");
});The wrapper never regenerates state; if a state file is missing/invalid it fails with instructions to run playwright-kit auth ensure.
- Auth states:
.auth/<profile>.json - Failures:
.auth/.failures/<profile>/<runId>/{trace.zip,screenshot.png,error.txt} - Locks:
.auth/.locks/<profile>.lock(prevents concurrentsetup/ensurefrom overwriting state)
Recommended:
.auth/
- Required when your
login()/validate()needs the app UI (viabaseURL), but the app is not guaranteed to be running beforeplaywright-kit auth ensure/setup(typical in CI). - Not needed if the app is already started externally (docker-compose, separate terminal/job) before running the auth CLI.
- Recommended for CI to avoid "connection refused" and to keep
ensureself-contained and deterministic.
npm iSee CONTRIBUTING.md.