Fix: legit logins only

This commit is contained in:
Zakaria
2026-05-16 13:57:02 -04:00
parent 677cae0125
commit 69ed1c78ad
78 changed files with 27211 additions and 8 deletions
@@ -0,0 +1,417 @@
# Fixtures & Hooks
## Table of Contents
1. [Built-in Fixtures](#built-in-fixtures)
2. [Custom Fixtures](#custom-fixtures)
3. [Fixture Scopes](#fixture-scopes)
4. [Hooks](#hooks)
5. [Authentication Patterns](#authentication-patterns)
6. [Database Fixtures](#database-fixtures)
## Built-in Fixtures
### Core Fixtures
```typescript
test("example", async ({
page, // Isolated page instance
context, // Browser context (cookies, localStorage)
browser, // Browser instance
browserName, // 'chromium', 'firefox', or 'webkit'
request, // API request context
}) => {
// Each test gets fresh instances
});
```
### Request Fixture
```typescript
test("API call", async ({ request }) => {
const response = await request.get("/api/users");
await expect(response).toBeOK();
const users = await response.json();
expect(users).toHaveLength(5);
});
```
## Custom Fixtures
### Basic Custom Fixture
```typescript
// fixtures.ts
import { test as base } from "@playwright/test";
// Declare fixture types
type MyFixtures = {
todoPage: TodoPage;
apiClient: ApiClient;
};
export const test = base.extend<MyFixtures>({
// Fixture with setup and teardown
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage); // Test runs here
// Teardown (optional)
await todoPage.clearTodos();
},
// Simple fixture
apiClient: async ({ request }, use) => {
await use(new ApiClient(request));
},
});
export { expect } from "@playwright/test";
```
### Fixture with Options
```typescript
type Options = {
defaultUser: { email: string; password: string };
};
type Fixtures = {
authenticatedPage: Page;
};
export const test = base.extend<Options & Fixtures>({
// Define option with default
defaultUser: [
{ email: "test@example.com", password: "pass123" },
{ option: true },
],
// Use option in fixture
authenticatedPage: async ({ page, defaultUser }, use) => {
await page.goto("/login");
await page.getByLabel("Email").fill(defaultUser.email);
await page.getByLabel("Password").fill(defaultUser.password);
await page.getByRole("button", { name: "Sign in" }).click();
await use(page);
},
});
// Override in config
export default defineConfig({
use: {
defaultUser: { email: "admin@example.com", password: "admin123" },
},
});
```
### Automatic Fixtures
```typescript
export const test = base.extend<{}, { setupDb: void }>({
// Auto-fixture runs for every test without explicit usage
setupDb: [
async ({}, use) => {
await seedDatabase();
await use();
await cleanDatabase();
},
{ auto: true },
],
});
```
## Fixture Scopes
### Test Scope (Default)
Created fresh for each test:
```typescript
test.extend({
page: async ({ browser }, use) => {
const page = await browser.newPage();
await use(page);
await page.close();
},
});
```
### Worker Scope
Shared across tests in the same worker (each worker gets its own instance; tests in different workers do not share it):
```typescript
type WorkerFixtures = {
sharedAccount: Account;
};
export const test = base.extend<{}, WorkerFixtures>({
sharedAccount: [
async ({ browser }, use) => {
// Expensive setup - runs once per worker
const account = await createTestAccount();
await use(account);
await deleteTestAccount(account);
},
{ scope: "worker" },
],
});
```
### Isolate test data between parallel workers
When tests in different workers touch the same backend or DB (e.g. same user, same tenant), they can collide and cause flaky failures. Use `testInfo.workerIndex` (or `process.env.TEST_WORKER_INDEX`) in a worker-scoped fixture to create unique data per worker:
```typescript
import { test as baseTest } from "@playwright/test";
type WorkerFixtures = {
dbUserName: string;
};
export const test = baseTest.extend<{}, WorkerFixtures>({
dbUserName: [
async ({}, use, testInfo) => {
const userName = `user-${testInfo.workerIndex}`;
await createUserInTestDatabase(userName);
await use(userName);
await deleteUserFromTestDatabase(userName);
},
{ scope: "worker" },
],
});
```
Then each worker uses a distinct user (e.g. `user-1`, `user-2`), so parallel workers do not overwrite each others data.
## Hooks
### beforeEach / afterEach
```typescript
test.beforeEach(async ({ page }) => {
// Runs before each test in file
await page.goto("/");
});
test.afterEach(async ({ page }, testInfo) => {
// Runs after each test
if (testInfo.status !== "passed") {
await page.screenshot({ path: `failed-${testInfo.title}.png` });
}
});
```
### beforeAll / afterAll
```typescript
test.beforeAll(async ({ browser }) => {
// Runs once before all tests in file
// Note: Cannot use page fixture here
});
test.afterAll(async () => {
// Runs once after all tests in file
});
```
### Describe-Level Hooks
```typescript
test.describe("User Management", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/users");
});
test("can list users", async ({ page }) => {
// Starts at /users
});
test("can add user", async ({ page }) => {
// Starts at /users
});
});
```
## Authentication Patterns
### Global Setup with Storage State
```typescript
// auth.setup.ts
import { test as setup, expect } from "@playwright/test";
const authFile = ".auth/user.json";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.TEST_EMAIL!);
await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await page.context().storageState({ path: authFile });
});
```
```typescript
// playwright.config.ts
export default defineConfig({
projects: [
{ name: "setup", testMatch: /.*\.setup\.ts/ },
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: ".auth/user.json",
},
dependencies: ["setup"],
},
],
});
```
### Multiple Auth States
```typescript
// auth.setup.ts
setup("admin auth", async ({ page }) => {
await login(page, "admin@example.com", "adminpass");
await page.context().storageState({ path: ".auth/admin.json" });
});
setup("user auth", async ({ page }) => {
await login(page, "user@example.com", "userpass");
await page.context().storageState({ path: ".auth/user.json" });
});
```
```typescript
// playwright.config.ts
projects: [
{
name: "admin tests",
testMatch: /.*admin.*\.spec\.ts/,
use: { storageState: ".auth/admin.json" },
dependencies: ["setup"],
},
{
name: "user tests",
testMatch: /.*user.*\.spec\.ts/,
use: { storageState: ".auth/user.json" },
dependencies: ["setup"],
},
];
```
### Auth Fixture
```typescript
// fixtures/auth.fixture.ts
export const test = base.extend<{ adminPage: Page; userPage: Page }>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: ".auth/admin.json",
});
const page = await context.newPage();
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: ".auth/user.json",
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
```
## Database Fixtures
This section covers **per-test database fixtures** (isolation, transaction rollback). For related topics:
- **Test data factories** (builders, Faker): See [test-data.md](test-data.md)
- **One-time database setup** (migrations, snapshots): See [global-setup.md](global-setup.md#database-patterns)
### Transaction Rollback Pattern
```typescript
import { test as base } from "@playwright/test";
import { db } from "../db";
export const test = base.extend<{ dbTransaction: Transaction }>({
dbTransaction: async ({}, use) => {
const transaction = await db.beginTransaction();
await use(transaction);
await transaction.rollback(); // Clean slate for next test
},
});
```
### Seed Data Fixture
```typescript
type TestData = {
testUser: User;
testProducts: Product[];
};
export const test = base.extend<TestData>({
testUser: async ({}, use) => {
const user = await db.users.create({
email: `test-${Date.now()}@example.com`,
name: "Test User",
});
await use(user);
await db.users.delete(user.id);
},
testProducts: async ({ testUser }, use) => {
const products = await db.products.createMany([
{ name: "Product A", ownerId: testUser.id },
{ name: "Product B", ownerId: testUser.id },
]);
await use(products);
await db.products.deleteMany(products.map((p) => p.id));
},
});
```
## Fixture Tips
| Tip | Explanation |
| ------------------ | ------------------------------------------- |
| Fixtures are lazy | Only created when used |
| Compose fixtures | Use other fixtures as dependencies |
| Keep setup minimal | Do heavy lifting in worker-scoped fixtures |
| Clean up resources | Use teardown in fixtures, not afterEach |
| Avoid shared state | Each fixture instance should be independent |
## Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
| ----------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Shared mutable state between tests | Race conditions, order dependencies | Use fixtures for isolation |
| Global variables in tests | Tests depend on execution order | Use fixtures or beforeEach for setup |
| Not cleaning up test data | Tests interfere with each other | Use fixtures with teardown or database transactions |
| Shared `page` or `context` in `beforeAll` | State leak between tests; flaky when tests run in parallel | Use default one-context-per-test, or `beforeEach` + fresh page; if serial is required, prefer `test.describe.configure({ mode: 'serial' })` and document that isolation is sacrificed |
| Backend/DB state shared across workers | Tests in different workers collide on same data | Use worker-scoped fixture with `testInfo.workerIndex` to create unique data per worker |
## Related References
- **Page Objects with fixtures**: See [page-object-model.md](page-object-model.md) for POM patterns
- **Test organization**: See [test-suite-structure.md](test-suite-structure.md) for test structure
- **Debugging fixture issues**: See [debugging.md](../debugging/debugging.md) for troubleshooting