Progressive onboarding for PCO — start at the level that matches your runner, then go deeper when you need MSW and routing.
Context: The README explains the toolset; vision.md walks a full booking scenario; why-pco.md adds diagrams and cross-runner depth.
| Level | You learn | Good first demo |
|---|---|---|
| Level 1 | ComponentTestObject + getters + primitives |
MuiPlayground.stories.tsx |
| Level 2 | Same DOM getters in Storybook + Cypress | cross-runner-tutorial.md |
| Level 3 | BaseViewTestObject, MSW, router shell |
vitest-demo |
Prerequisites: React 18+, React Router v6/v7 for Level 3. Install: install.md.
DOM-only test objects — no App, no MSW, no router. Ideal for Storybook play and MUI preset demos.
import { ComponentTestObject } from '@page-component-object/queries';
export class CatalogHomeStoryTestObject extends ComponentTestObject {
get heading() {
return this.context.getByRole('heading', { name: /items/i });
}
get itemLinks() {
return this.context.getAllByRole('link');
}
}Storybook: createStoryPlay binds the story canvas:
play: createStoryPlay(
() => new CatalogHomeStoryTestObject(),
async (_canvas, view) => {
expect(view.heading).toBeTruthy();
await view.itemLinks[0].userClick();
},
),MUI widgets: @page-component-object/preset-mui — see presets/mui.md.
Storybook only:
// .storybook/preview.ts
import { setupPCOStorybook } from '@page-component-object/adapter-storybook';
setupPCOStorybook();Vitest/Jest not required at this level.
Reuse getter definitions across Storybook and Cypress. Cypress does not use MSW — bind to the live AUT document.
// Cypress — after cy.visit
const view = new CatalogHomeStoryTestObject();
view.bindToRoot(document.body);
// Prefer CatalogHomeCypressTestObject + PCOChainable for retry — see cypress-adoption.mdFull walkthrough: cross-runner-tutorial.md.
Cypress setup:
// cypress/support/e2e.ts
import '@testing-library/cypress/add-commands';
import { setupPCOCypress } from '@page-component-object/adapter-cypress';
setupPCOCypress();View test objects with render(), API mocks, and optional full-app navigation.
pnpm add @page-component-object/core @page-component-object/queries @page-component-object/msw \
@page-component-object/react @page-component-object/router-react \
@page-component-object/adapter-vitest// vitest — src/setup.ts
import { installPCOLifecycle } from '@page-component-object/adapter-vitest';
import { configureViewTestObjects } from '@page-component-object/react';
import { createDemoAppManager } from './testing/DemoAppManager';
configureViewTestObjects({ createAppManager: createDemoAppManager });
installPCOLifecycle({ apiBaseUrl: 'http://localhost' });Equivalent: setupPCO() (Vitest), setupPCOJest() (Jest), setupPCOStorybook() (Storybook).
| Layer | Class | Role |
|---|---|---|
| Query + primitive | ComponentTestObject |
Getters + userClick / userType |
| View + app + API mocks | BaseViewTestObject |
Above + setupMockData() + render() |
Intent methods (fillLogin, openSettings) belong on your *.to.* classes — see philosophy.md.
Colocate under __pco__ beside features. Data factories in *.factory.ts; API mocks in *Api.to.ts. Layout: project-structure.md.
JSX rule: files with render() / JSX must be *.to.tsx.
import { BaseViewTestObject } from '@page-component-object/react';
export class HomeViewTestObject extends BaseViewTestObject {
itemsApi = new ItemsApiTestObject();
items = ItemFactory.defaultList(3);
readonly mocks = {
getItems: null as ReturnType<ItemsApiTestObject['registerGetItems']> | null,
};
setupMockData() {
this.mocks.getItems = this.itemsApi.registerGetItems(() => this.items);
return this.mocks;
}
async render() {
return this.app.renderView(<Home items={this.items} loading={false} />, {
route: '/',
routePath: '/',
});
}
}const view = new HomeViewTestObject();
await view.render();
expect(view.heading).toBeTruthy();
expect(view.mocks.getItems).toBeTruthy();| Mode | API | When |
|---|---|---|
| Shallow | view.render() |
Single screen |
| Full app | view.renderApp() |
Multi-route navigation |
After navigation, create a new view test object or use view.getHistory().
Pass AppRoutes (route tree only) to renderApp() — not production App with BrowserRouter. See troubleshooting in prior docs if you see nested router errors.
export const Default: Story = {
parameters: HomeViewTestObject.storyParameters(),
};Details: msw-storybook.md.
App.get() is a module singleton for the test app shell. View test objects call this.app.renderView() — they do not own the manager.
| Rule | Detail |
|---|---|
| Registration | configureViewTestObjects({ createAppManager }) in test setup |
| Access | BaseViewTestObject uses App.get() internally |
| Isolation | Adapters call App.reset() in afterEach — always use installPCOLifecycle / Jest equivalent |
| Parallel tests | Same-file concurrent view tests sharing App are unsupported — rare in practice |
DOM-only test objects (Level 1–2) never touch App.
DataFactory (formerly ObjectFactory) builds fixture data — not test object instances. See project-structure.md.
pnpm install && pnpm build
pnpm --filter @page-component-object/vitest-demo test
pnpm --filter @page-component-object/jest-demo test
pnpm --filter @page-component-object/storybook-demo storybook
pnpm --filter @page-component-object/cypress-demo test| Doc | Topic |
|---|---|
| Why PCO | Central thesis |
| Cross-runner tutorial | One view, three runners |
| Install | Peers + compatibility matrix |
| Resolver model | rootResolver architecture |
| Cypress adoption | Chainables + E2E path |
| Philosophy | Query → primitive → intent |