Sprint 2 β E2E Utils# Sprint 2 β E2E Utils (Factory-Based Architecture)
Status: π PLANNED Status: π PLANNED
Duration: 2 weeks Duration: 2 weeks
Sprint Goal: Ship kernel-aware E2E utilities that complement WordPress's Playwright fixtures with memorable, typed wrappersSprint Goal: Ship @geekist/wp-kernel-e2e-utils with factory-based utilities that complement WordPress's Playwright fixtures while exposing all underlying primitives
---Core Philosophy: Annotate & Expose, Never Hide β Wrap WordPress utilities with kernel-aware factories while always exposing the underlying fixtures for power users.
Goal---
Extend @wordpress/e2e-test-utils-playwright with kernel-aware utilities for testing resources, stores, and events. Utilities work with our existing kernel primitives (defineResource, store keys, event taxonomy) without duplicating WordPress functionality.## Goal
---Provide a stable, production-ready E2E testing package that extends @wordpress/e2e-test-utils-playwright with kernel-aware capabilities through composable factory functions. Developers should be able to test resources, stores, and events without duplicating WordPress utilities or breaking when WordPress updates.
Scope---
In Scope:## Scope
-
Kernel-aware utilities:
seed(),waitForStore(),captureEvents()What's In Scope: -
Fixture extension:
testwithkernelhelper exposing WordPress fixtures + kernel utilities -
Works with existing kernel code:
defineResource,job.storeKey,wpk.*events- Factory Functions:createResourceUtils,createStoreUtils,createEventCapture(foundation only) -
Full TypeScript typing with generics- Fixture Extension: Extend WordPress's
testfixture withkernelhelper that exposes:- Factory methods for common operations (90% usage)
Out of Scope: - All WordPress fixtures (requestUtils, admin, editor, pageUtils, page)
-
TypeScript Generics: Fully typed factories with generic type inference
-
Policy testing (Sprint 3), Action testing (Sprint 4), Job polling (Sprint 8)- Tests: Unit tests for factories, integration tests with WordPress utils
-
Database snapshots, visual regression, performance benchmarking- Docs: Factory API reference, usage examples, "Creating Custom Factories" guide
---What's Out of Scope (Deferred):
User Stories- Full event taxonomy validation β Sprint 4 (Actions & Events)
- Policy testing utilities β Sprint 3 (Policies)
-
As a developer, I can seed resource data using
seed(requestUtils, resource, data)instead of raw REST calls- Job polling utilities β Sprint 8 (Jobs) -
As a developer, I can wait for store data with
waitForStore(page, storeKey, selector)without polling loops- PHP bridge event testing β Sprint 9 (PHP Bridge) -
As a developer, I can capture events with
captureEvents(page)and assert on kernel event emission- Visual regression testing β Sprint 17 (Hardening) -
As a power user, I can access WordPress's
requestUtils,admin,pagedirectly when needed
User Stories
Examples with Real Kernel Code
- As a developer, I can seed resource data using a typed factory without writing raw REST calls
Our Existing Kernel Primitives2. As a developer, I can wait for store data to load without polling loops
- As a developer, I can capture and assert on kernel events for testing event-driven behavior
```typescript4. As a power user, I can drop down to WordPress's requestUtilsor`page` when I need low-level control
// From @geekist/wp-kernel5. As a framework user, I can create custom factories by extending the base factories without forking code
const job = defineResource({
name: 'job',---
routes: {
list: { path: '/wpk/v1/jobs', method: 'GET' },## Definition of Done
create: { path: '/wpk/v1/jobs', method: 'POST' },
}### Package Deliverables
});- β Three core factories implemented:
createResourceUtils(seed, seedMany, remove, deleteAll)
// Provides: - createStoreUtils (wait, invalidate, getState)
job.create(data) // REST client method - createEventCapture (list, find, findAll, clear, stop)
job.storeKey // 'wpk/job'- β
Fixture extends WordPress test with kernel helper
job.invalidate() // Cache invalidation- β
All WordPress fixtures exposed through kernel (never hidden)
// Events: wpk.resource.request, wpk.resource.response, etc.- β TypeScript generics fully typed with inference
### Example 1: Seed Resource Data### Testing
- β
Unit tests for each factory (β₯90% coverage)
```typescript- β
Integration tests showing factories working with WordPress utils
import { test, expect } from '@geekist/wp-kernel-e2e-utils';- β
Type tests verifying generic inference
- β
Example tests demonstrating all three usage styles
test('seed jobs', async ({ kernel }) => {
// Pass our actual resource config### Documentation
const utils = kernel.resource(job);- β
Factory API reference with full signatures
- β
"Annotate & Expose" philosophy explained
await utils.seed({ title: 'Engineer', salary: 100000 });- β
Usage examples for each factory
// Behind scenes: calls job.create() from our kernel- β
"Creating Custom Factories" guide
- β
Migration examples from WordPress utils
await utils.seedMany([- β
Three usage styles documented (fixture, direct, escape hatch)
{ title: 'Designer' },
{ title: 'Manager' }### Quality
]);- β
Zero TypeScript errors
});- β
Tree-shaking verified
```- β
No WordPress util duplication
- β
Performance benchmark: < 100ms fixture overhead per test
### Example 2: Wait for Store- β
CI green (all tests passing)
```typescript---
test('wait for jobs', async ({ kernel }) => {
await kernel.page.goto('/wp-admin/admin.php?page=wpk-jobs');## Architecture Overview
// Use our actual storeKey### Philosophy: Annotate & Expose, Never Hide
const store = kernel.store(job.storeKey); // 'wpk/job'
```typescript
const jobs = await store.wait(s => s.getList());// β Wrong: Hide WordPress utilities behind abstraction
expect(jobs.items.length).toBeGreaterThan(0);kernel: {
}); doThing() { /* uses requestUtils internally */ }
```}
### Example 3: Capture Events// β
Right: Expose everything, provide convenience layer
kernel: {
```typescript // Factory methods for common operations (90% usage)
test('capture kernel events', async ({ kernel }) => { resource: (config) => createResourceUtils(config, requestUtils),
const recorder = await kernel.events({ pattern: /^wpk\./ }); store: (storeName) => createStoreUtils(storeName, page),
events: (opts?) => createEventCapture(page, opts),
await kernel.page.click('[data-testid="create-job"]');
await kernel.page.fill('[name="title"]', 'New Job'); // Full access to WordPress fixtures (always available)
await kernel.page.click('[data-testid="submit"]'); requestUtils, // β
WordPress REST utilities
admin, // β
WordPress admin navigation
// Assert on our actual events editor, // β
WordPress block editor
expect(recorder.find('wpk.resource.request')).toBeTruthy(); pageUtils, // β
WordPress page utilities
expect(recorder.find('wpk.resource.response')).toBeTruthy(); page, // β
Playwright page
}
await recorder.stop();```
});
```### Fixture Extension Pattern
### Example 4: Power User - Direct Access```typescript
// src/fixture.ts
```typescriptimport { test as base } from '@wordpress/e2e-test-utils-playwright';
test('custom operation', async ({ kernel }) => {import { createResourceUtils, createStoreUtils, createEventCapture } from './factories';
// Drop down to WordPress utils when needed
await kernel.requestUtils.rest({export const test = base.extend({
path: '/wpk/v1/jobs/batch-import', kernel: async ({ page, requestUtils, admin, editor, pageUtils }, use) => {
method: 'POST', await use({
data: { source: 'csv', rows: [...] } // Factory methods
}); resource: <T>(config: ResourceConfig<T>) =>
createResourceUtils(config, requestUtils),
// Or use page directly store: (storeName: string) =>
await kernel.page.evaluate(() => { createStoreUtils(storeName, page),
window.wp.data.dispatch('wpk/job').receiveItems([...]); events: (opts?: EventCaptureOptions) =>
}); createEventCapture(page, opts),
});
``` // Expose all WordPress fixtures
requestUtils,
--- admin,
editor,
## Architecture pageUtils,
page,
### Fixture Extension });
}
```typescript});
// src/fixture.ts
import { test as base } from '@wordpress/e2e-test-utils-playwright';export { expect } from '@wordpress/e2e-test-utils-playwright';
export const test = base.extend({
kernel: async ({ page, requestUtils, admin, editor, pageUtils }, use) => {---
await use({
// Kernel utilities (wraps our defineResource, storeKey, events)## Factory Implementations
resource: (resourceObj) => createResourceUtils(resourceObj, requestUtils),
store: (storeKey) => createStoreUtils(storeKey, page),### 1. createResourceUtils
events: (opts?) => createEventCapture(page, opts),
**Purpose**: Generic CRUD operations for any kernel resource
// WordPress fixtures (always exposed)
requestUtils,**Interface**:
admin,```typescript
editor,interface ResourceConfig<T> {
pageUtils, name: string;
page, routes: {
}); list?: { path: string; method: 'GET' };
} create?: { path: string; method: 'POST' };
}); remove?: { path: string; method: 'DELETE' };
};
### Utility Implementations}
**`createResourceUtils(resourceObj, requestUtils)`**function createResourceUtils<T>(
- Takes our `defineResource` output (job, application, etc.) config: ResourceConfig<T>,
- Returns: `seed()`, `seedMany()`, `remove()`, `deleteAll()` requestUtils: RequestUtils
- Uses `resourceObj.create()`, `resourceObj.remove()` internally): {
seed(data: Partial<T>): Promise<T>;
**`createStoreUtils(storeKey, page)`** seedMany(rows: Partial<T>[]): Promise<T[]>;
- Takes our `job.storeKey` ('wpk/job') remove(id: string | number): Promise<void>;
- Returns: `wait()`, `invalidate()`, `getState()` deleteAll(): Promise<void>;
- Polls `select(storeKey).getList()` until data appears requestUtils: RequestUtils; // Exposed for advanced usage
}
**`createEventCapture(page, opts)`**```
- Captures `wpk.*` events from our kernel
- Returns: `list()`, `find()`, `findAll()`, `clear()`, `stop()`**Usage**:
- Injects listener via `wp.hooks.addAction('all', ...)````typescript
test('seed jobs', async ({ kernel }) => {
--- const job = kernel.resource<Job>({
name: 'job',
## Why This Approach routes: {
list: { path: '/wpk/v1/jobs', method: 'GET' },
| Aspect | Benefit | create: { path: '/wpk/v1/jobs', method: 'POST' },
|--------|---------| remove: { path: '/wpk/v1/jobs/:id', method: 'DELETE' },
| **Works with kernel code** | Uses `defineResource`, `storeKey`, events we already have | },
| **Memorable names** | `seed()`, `waitForStore()` vs raw `requestUtils.rest()` | });
| **WordPress compatible** | Extends fixtures, doesn't replace them |
| **Future-proof** | Same pattern for Actions (Sprint 4), Jobs (Sprint 8) | await job.seed({ title: 'Engineer', salary: 100000 });
| **Power user friendly** | All WordPress fixtures exposed | await job.seedMany([{ title: 'Designer' }, { title: 'Manager' }]);
await job.deleteAll();
---});
Definition of Done
Package Deliverables
-
β
createResourceUtils,createStoreUtils,createEventCaptureimplemented### 2. createStoreUtils -
β Fixture extends WordPress
testwithkernelhelper -
β Works with existing
defineResourceoutputPurpose: Wait for and interact with@wordpress/datastores -
β All WordPress fixtures exposed (never hidden)
-
β TypeScript generics for resource typesInterface:
### Testinginterface StoreWaitOptions {
- β
Unit tests for each utility (β₯90% coverage) timeoutMs?: number;
- β
Integration tests with actual kernel resources intervalMs?: number;
- β
Type tests verifying inference with `defineResource`}
- β
Example tests in showcase app
function createStoreUtils(storeName: string, page: Page): {
### Documentation wait<T>(selector: (store: any) => T, opts?: StoreWaitOptions): Promise<T>;
- β
API reference: `kernel.resource()`, `kernel.store()`, `kernel.events()` invalidate(keys: unknown[]): Promise<void>;
- β
Examples with real kernel code (job resource) getState(): Promise<any>;
- β
"Annotate & Expose" philosophy page: Page; // Exposed for advanced usage
- β
Migration from raw WordPress utils}Quality
-
β Zero TypeScript errorsUsage:
-
β Performance: < 100ms fixture overhead```typescript
-
β CI green on all teststest('wait for store data', async ({ kernel }) => {
await kernel.page.goto('/wp-admin/admin.php?page=wpk-jobs');
const jobStore = kernel.store('wpk/job');
Timeline (2 Weeks)
const jobs = await jobStore.wait(
Week 1 (store) => store.getList(),
Days 1-3: Utilities { timeoutMs: 3000 }
-
createResourceUtilswithdefineResourceintegration ); -
createStoreUtilswith store key polling -
createEventCapturewith hook injection expect(jobs.items.length).toBeGreaterThan(0);
});
Days 4-5: Fixture```
-
Extend WordPress
testfixture -
Wire up utilities to
kernelhelper--- -
Ensure all WordPress fixtures exposed
3. createEventCapture
Week 2
**Days 6-8: Testing & Docs****Purpose**: Capture and assert on kernel events (foundation for Sprint 4)
-
Unit tests for utilities
-
Integration tests with kernel resourcesInterface:
-
API reference and examples```typescript
interface EventCaptureOptions {
Days 9-10: Polish pattern?: RegExp;
-
Migrate showcase tests includePayload?: boolean;
-
Performance benchmarks}
-
Final review
interface EventRecord {
--- name: string;
payload?: unknown;
Future Extensions timestamp: number;
}
Same pattern for upcoming sprints:
async function createEventCapture(
// Sprint 3: Policies opts?: EventCaptureOptions
kernel.policy(policyConfig).check(user, resource);): Promise<{
list(): Promise<EventRecord[]>;
// Sprint 4: Actions find(name: string): Promise<EventRecord | undefined>;
kernel.action(actionConfig).invoke(params); findAll(pattern: RegExp): Promise<EventRecord[]>;
clear(): Promise<void>;
// Sprint 8: Jobs stop(): Promise<void>;
kernel.job(jobConfig).enqueue(data).wait();}>---Usage:
## Success Metricstest('capture resource events', async ({ kernel }) => {
const recorder = await kernel.events({ pattern: /^wpk\.job\./ });
- **3 utilities** shipped: resource, store, events
- **Works with kernel** - `defineResource`, `storeKey`, `wpk.*` events await kernel.page.click('[data-testid="create-job"]');
- **β₯90% test coverage** for utilities await kernel.page.fill('[name="title"]', 'New Job');
- **100% WordPress fixtures** exposed await kernel.page.click('[data-testid="submit"]');
- **< 100ms overhead** per test
const createdEvent = await recorder.find('wpk.job.created');
--- expect(createdEvent).toBeTruthy();
## References await recorder.stop();
});
- [WordPress E2E Testing Guide](https://developer.wordpress.org/block-editor/contributors/code/testing-overview/e2e/)```
- [E2E Utils Package Specification](../E2E%20Utils%20Package%20Specification.md)
- [Sprint 1 Retrospective](./sprint_1_retro.md)---
---## Three Usage Styles
**Next Sprint**: Sprint 3 β Policies (Client hints + Server parity)### 1. Via Fixture (Recommended)
```typescript
import { test, expect } from '@geekist/wp-kernel-e2e-utils';
test('with fixture', async ({ kernel }) => {
const job = kernel.resource(jobConfig);
await job.seed({ title: 'Engineer' });
});2. Direct Factory Import (Custom Setup)
import { createResourceUtils } from '@geekist/wp-kernel-e2e-utils';
test('direct', async ({ requestUtils }) => {
const job = createResourceUtils(jobConfig, requestUtils);
await job.seed({ title: 'Engineer' });
});3. Drop Down to WordPress Utils (Escape Hatch)
test('escape hatch', async ({ kernel }) => {
// Use WordPress utilities directly when needed
await kernel.requestUtils.rest({
path: '/wpk/v1/custom-endpoint',
method: 'POST',
data: { custom: 'payload' }
});
// Or manipulate page directly
await kernel.page.evaluate(() => {
window.wp.data.dispatch('wpk/job').receiveItems([...]);
});
});Real-World Example: Full Workflow Test
import { test, expect } from '@geekist/wp-kernel-e2e-utils';
test('create and list jobs', async ({ kernel }) => {
// Navigate using WordPress admin fixture (exposed through kernel)
await kernel.admin.visitAdminPage('admin.php?page=wpk-jobs');
// Seed data using kernel resource factory
const job = kernel.resource<Job>({
name: 'job',
routes: {
list: { path: '/wpk/v1/jobs', method: 'GET' },
create: { path: '/wpk/v1/jobs', method: 'POST' },
remove: { path: '/wpk/v1/jobs/:id', method: 'DELETE' },
},
});
await job.seedMany([
{ title: 'Engineer', salary: 100000 },
{ title: 'Designer', salary: 90000 },
]);
// Wait for store to update using kernel store factory
const jobStore = kernel.store('wpk/job');
const jobs = await jobStore.wait((s) => s.getList());
expect(jobs.items).toHaveLength(2);
// Cleanup
await job.deleteAll();
});Package Structure
packages/e2e-utils/
βββ src/
β βββ factories/
β β βββ createResourceUtils.ts
β β βββ createStoreUtils.ts
β β βββ createEventCapture.ts
β β βββ index.ts
β βββ fixture.ts # Extends WordPress test fixture
β βββ index.ts # Main exports
βββ tests/
β βββ factories/
β β βββ resource.spec.ts
β β βββ store.spec.ts
β β βββ events.spec.ts
β βββ integration/
β βββ fixture.spec.ts
βββ package.json
βββ README.md
Why Factory-Based vs Hardcoded Utilities?
| Aspect | Hardcoded Utilities | Factory Functions |
|---|---|---|
| Maintenance | 11+ utilities to maintain | 3 factories (composable) |
| WordPress Updates | Manual sync required | Automatic compatibility |
| Extensibility | Fixed utilities | Infinite via composition |
| Type Safety | Per-util types | Generic types flow through |
| Bundle Size | Larger (hardcoded) | Minimal (factory code) |
| Sprint Dependencies | Depends on Sprint 4 events | No dependencies |
| User Customization | Fork or submit PR | Create custom factory |
| Learning Curve | 11 new utilities to learn | 3 factory patterns |
| Power User Access | Limited | Full (all fixtures exposed) |
Timeline (2 Weeks)
Week 1
Days 1-3: Core Factories
- Implement
createResourceUtilswith TypeScript generics - Implement
createStoreUtilswith wait/invalidate - Implement
createEventCapture(foundation only)
Days 4-5: Fixture Integration
- Extend WordPress test fixture
- Wire up factories to kernel fixture
- Ensure all WordPress fixtures exposed
- Write fixture integration tests
Week 2
Days 6-8: Testing & Documentation
- Unit tests for all factories (β₯90% coverage)
- Integration tests with WordPress utils
- Type tests for generic inference
- API reference documentation
- Usage examples for all three styles
Days 9-10: Migration & Polish
- Migrate showcase E2E tests to new utilities
- Performance benchmarks (< 100ms overhead)
- Final documentation review
- Changeset created
- Ready for release
Success Metrics
Quantitative
- 3 core factories shipped
- β₯90% test coverage for factories
- 100% fixture exposure (no WordPress utils hidden)
- < 100ms overhead per test from fixture
- Zero dependencies on future sprints
Qualitative
- Developers can guess the API correctly
- Power users can drop down to WordPress utils effortlessly
- Custom factories can be created in < 30 lines of code
- Tests feel like natural Playwright + WordPress patterns
- No confusion about when to use factories vs WordPress utils
Migration Path
Phase 1: Core Factories (Sprint 2)
- β
createResourceUtils- Generic resource operations - β
createStoreUtils- Store waiting/invalidation - β
createEventCapture- Foundation event capture - β Fixture extension (exposes all WordPress fixtures)
Phase 2: Enhanced Factories (Sprint 3+)
createPolicyUtils- Policy testing (Sprint 3)createActionUtils- Action lifecycle testing (Sprint 4)createJobUtils- Job polling/status (Sprint 8)
Phase 3: Community Factories
- Users create their own factories
- Share via npm packages or gists
- Framework remains minimal and focused
Risks & Mitigations
Risk: Complexity of Generic Types
Likelihood: Medium
Impact: Medium
Mitigation: Start with simple types, add complexity iteratively. Provide comprehensive type examples in documentation.
Risk: Event Capture Performance Overhead
Likelihood: Low
Impact: Medium
Mitigation: Make payload capture opt-in. Benchmark with 1000+ events. Document performance characteristics.
Risk: Breaking Changes from WordPress Updates
Likelihood: Low
Impact: Low
Mitigation: We only extend, never fork. WordPress changes flow through automatically. Integration tests catch breaking changes.
Risk: Users Don't Understand Factory Pattern
Likelihood: Medium
Impact: Medium
Mitigation: Clear examples, comparison with old approach, "Creating Custom Factories" guide, video walkthrough.
Dependencies
Required (Sprint 1 Complete)
- β Resources implemented and stable
- β Store integration working
- β
Resource events foundation (
wpk.resource.*) - β Showcase plugin exists with working tests
Enables Future Sprints
- Sprint 3 (Policies): Test policy enforcement with
resourcefactory andrequestUtils - Sprint 4 (Actions): Test action events with enhanced
createEventCapture - Sprint 8 (Jobs): Test job lifecycle with
createJobUtilsfactory - Sprint 9 (PHP Bridge): Test bridged events with event capture utilities
Out of Scope (Future Sprints)
- Sprint 4: Full event taxonomy validation, event payload schema checking
- Sprint 9: PHP bridge event testing, sync/async execution testing
- Sprint 13: Multi-version WordPress testing, flake detection
- Sprint 17: Visual regression, performance benchmarking, accessibility testing
References
- E2E Utils Package Specification
- Roadmap Β§ Sprint 2
- Sprint 1 Retrospective
- WordPress E2E Utils
- Playwright Fixtures Documentation
Next Sprint: Sprint 3 β Policies (Client hints + Server parity)
List view
0 issues of 0 selected
There are no open issues in this milestone
Add issues to milestones to help organize your work for a particular release or project. Find and add issues with no milestones in this repo.