Skip to content
Open
No due date
β€’Last updated Oct 3, 2025

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: test with kernel helper 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 test fixture with kernel helper 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)
  1. As a developer, I can seed resource data using seed(requestUtils, resource, data) instead of raw REST calls- Job polling utilities β€” Sprint 8 (Jobs)

  2. 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)

  3. As a developer, I can capture events with captureEvents(page) and assert on kernel event emission- Visual regression testing β€” Sprint 17 (Hardening)

  4. As a power user, I can access WordPress's requestUtils, admin, page directly when needed



User Stories

Examples with Real Kernel Code

  1. 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

  1. 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, createEventCapture implemented### 2. createStoreUtils

  • βœ… Fixture extends WordPress test with kernel helper

  • βœ… Works with existing defineResource outputPurpose: Wait for and interact with @wordpress/data stores

  • βœ… 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 }

  • createResourceUtils with defineResource integration );

  • createStoreUtils with store key polling

  • createEventCapture with hook injection expect(jobs.items.length).toBeGreaterThan(0);

});

Days 4-5: Fixture```

  • Extend WordPress test fixture

  • Wire up utilities to kernel helper---

  • 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 createResourceUtils with TypeScript generics
  • Implement createStoreUtils with 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 resource factory and requestUtils
  • Sprint 4 (Actions): Test action events with enhanced createEventCapture
  • Sprint 8 (Jobs): Test job lifecycle with createJobUtils factory
  • 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


Next Sprint: Sprint 3 β€” Policies (Client hints + Server parity)

100% complete

List view

    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.