📝e2e-testing/11-as-seeding.md
Inline Data Seeding: The .as() Method
category: "E2E Testing"
Inline Data Seeding: The .as() Method
The Goal
Seed data right where you need it instead of hiding it in beforeEach:
await page.goto('/dashboard')
.as(testData.user().withDefaults().create())
.perform(ClickOn('new-post'))
.verify(Shows('Welcome'));This is what keeps setup local to the test and out of shared hooks.
Implementing the .as() Extension
We extend Playwright's Page or create a wrapper:
// src/pages/extensions/FluentPage.ts
import { Page, Locator } from '@playwright/test';
import { TestDataFactory } from '../../factories/TestDataFactory';
export class FluentPage {
private page: Page;
private seededData: any = null;
constructor(page: Page) {
this.page = page;
}
// ============ Navigation ============
goto(url: string): this {
this.page.goto(url);
return this;
}
async click(selector: string): Promise<this> {
await this.page.click(selector);
return this;
}
async fill(selector: string, value: string): Promise<this> {
await this.page.fill(selector, value);
return this;
}
// ============ The .as() Method ============
async as<T>(factoryMethod: () => Promise<T>): Promise<this> {
// Create the test data BEFORE continuing
this.seededData = await factoryMethod();
return this;
}
// Access seeded data in tests
getData<T>(): T {
return this.seededData as T;
}
// ============ Assertion ============
async verify(matcher: (page: Page) => Promise<void>): Promise<void> {
await matcher(this.page);
}
// ============ Wrap existing page methods ============
get locator() {
return this.page.locator.bind(this.page);
}
get waitForSelector() {
return this.page.waitForSelector.bind(this.page);
}
// Delegate other methods
async click(selector: string, options?: any): Promise<this> {
await this.page.click(selector, options);
return this;
}
}Creating a Test Fixture
// src/fixtures/index.ts
import { test as base, Page } from '@playwright/test';
import { FluentPage } from '../pages/extensions/FluentPage';
export const test = base.extend<{
page: Page & { asPage: () => FluentPage };
}>({
page: async ({ page }, use) => {
// Add asPage() to every page
const fluentPage = new FluentPage(page);
// Extend page with fluent methods
const extendedPage = Object.assign(page, {
asPage: () => fluentPage,
as<T>(factoryMethod: () => Promise<T>): Promise<FluentPage> {
return fluentPage.as(factoryMethod);
},
});
await use(extendedPage as any);
},
});
export { expect } from '@playwright/test';Using .as() in Tests
import { test, expect } from '../fixtures';
test('user sees their profile after login', async ({ page }) => {
await page.goto('/')
.as(async () => {
// Create user and get token
const { user } = await testData.user()
.withUsername('testuser')
.withDefaults()
.create();
return user;
})
.click('text=Sign in')
.fill('input[type=email]', 'testuser@test.com')
.fill('input[type=password]', 'password123')
.click('button[type=submit]');
// Verify
await expect(page.locator('text=testuser')).toBeVisible();
});
// Even cleaner - seed BEFORE navigation
test('dashboard with seeded user', async ({ page, testData }) => {
// Create data first
const { user, articles } = await testData.post()
.withAllDeps()
.create();
// Then use it in test
await page.goto('/dashboard')
.fill('input[placeholder=email]', user.email)
.fill('input[placeholder=password]', 'password123')
.click('button:has-text("Login")');
// Verify seeded content
await expect(page.locator(`text=${articles[0].title}`)).toBeVisible();
});The .perform() Pattern
// .perform() executes a sequence of actions
async perform(...actions: (() => Promise<void>)[]): Promise<this> {
for (const action of actions) {
await action();
}
return this;
}
// Usage
await page.goto('/editor')
.perform(
() => page.fill('input[title]', 'My Post'),
() => page.fill('textarea[body]', 'Content'),
() => page.click('button:has-text("Publish")')
);The .verify() Pattern
// .verify() runs assertions
async verify(assertions: () => Promise<void>): Promise<void> {
await assertions();
}
// Usage
await page.goto('/article/my-article')
.verify(async () => {
await expect(page.locator('h1')).toHaveText('My Article');
await expect(page.locator('.author')).toHaveText('testuser');
});Complete 2-Line Test
test('complete user flow', async ({ page, testData }) => {
// Arrange and Act
await page.goto('/editor')
.as(testData.user().withDefaults().create())
.perform(ArticleForm.create()
.withTitle('My New Post')
.withDescription('Short summary')
.withBody('Great content here')
.withTags(['testing'])
.fill()
)
.click('button:has-text("Publish")');
await page.verify(async () => {
await expect(page.locator('h1')).toHaveText('My New Post');
});
});What We've Learned
- Implemented .as() for local inline seeding
- Created FluentPage wrapper
- Added .perform() and .verify() methods
- Removed the need for bulky shared setup hooks
Next: Putting it all together for true 2-line tests.
Previous: Fluent Form Builders: The FormBuilder Pattern Next: Putting It Together: 2-Line E2E Tests That Stay Honest ->