Initial Commit
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
# OpenSpec Change: enterprise-browser-storybook-test-utils
|
||||
|
||||
- **Change ID**: enterprise-browser-storybook-test-utils
|
||||
- **Package**: `@suaveplan/storybook-test-utils`
|
||||
- **Category**: browser
|
||||
- **Target path**: `browser/dev/storybook-test-utils`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/storybook-test-utils.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/storybook-test-utils` on the genesis branch at enterprise quality. The package provides Playwright helpers for driving Storybook preview UIs across every Suaveplan React package. Genesis: splits the single `storybook-helpers.ts` into six focused modules, ships a `STORYBOOK_PORTS` registry, adds typed story-index discovery, console/network/accessibility assertion helpers, a reusable Playwright `test` fixture, moves `@playwright/test` to `peerDependencies`, writes the currently-zero test coverage (≥95%), and authors all missing `.md` docs.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
Fresh build — does not exist on genesis yet.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logging for helper diagnostics |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
`peerDependencies`: `@playwright/test` (used in public API types — must not be a dev-only dep).
|
||||
|
||||
Dev: `vitest`, `@vitest/coverage-istanbul`, `@playwright/test`, `@suaveplan/testing`.
|
||||
|
||||
No React runtime dep — this package is a Playwright helper library.
|
||||
|
||||
---
|
||||
|
||||
## Preserved public API surface
|
||||
|
||||
```ts
|
||||
// Navigation
|
||||
function navigateToStory(page: Page, meta: StoryMetadata): Promise<void>
|
||||
function navigateToStoryWithUI(page: Page, meta: StoryMetadata): Promise<void>
|
||||
function switchToStory(page: Page, meta: StoryMetadata): Promise<void>
|
||||
function reloadStory(page: Page): Promise<void>
|
||||
|
||||
// Waiting
|
||||
function waitForStoryLoaded(page: Page, options?: WaitOptions): Promise<void>
|
||||
function waitForStorybookIframe(page: Page, options?: WaitOptions): Promise<FrameLocator>
|
||||
function waitForStoryInteractive(page: Page, options?: WaitOptions): Promise<void>
|
||||
|
||||
// Iframe access
|
||||
function getStorybookFrame(page: Page): FrameLocator
|
||||
function getStoryCanvas(frame: FrameLocator): Locator
|
||||
|
||||
// Args
|
||||
function getStoryArgs(page: Page): Promise<Record<string, unknown>>
|
||||
function updateStoryArgs<T = Record<string, unknown>>(page: Page, args: Partial<T>): Promise<void>
|
||||
|
||||
// Interactions
|
||||
function clickWithFallback(locator: Locator): Promise<void>
|
||||
function getClickableElement(frame: FrameLocator, selector: string): Locator
|
||||
|
||||
// Forms
|
||||
function getFormFromStory(frame: FrameLocator, selector?: string): Locator
|
||||
|
||||
// Port registry (new)
|
||||
const STORYBOOK_PORTS: Record<string, number>
|
||||
function storyUrl(pkg: string, storyId: string, opts?: UrlOptions): string
|
||||
|
||||
// Story index (new)
|
||||
function loadStoryIndex(baseUrl: string): Promise<StoryIndex>
|
||||
function defineStoryId(kind: string, name: string): string
|
||||
|
||||
// Assertions (new)
|
||||
function expectNoConsoleErrors(page: Page, opts?: { allow?: RegExp[] }): Promise<void>
|
||||
function expectNoFailedRequests(page: Page): Promise<void>
|
||||
function expectAllFontsLoaded(page: Page): Promise<void>
|
||||
function auditAccessibility(page: Page, opts?: AxeOptions): Promise<AxeResult>
|
||||
|
||||
// Fixtures (new)
|
||||
const test: typeof baseTest // extended with storybook fixture
|
||||
```
|
||||
|
||||
Subpath exports: `"."`, `"./navigation"`, `"./waiting"`, `"./iframe"`, `"./args"`, `"./interactions"`, `"./forms"`, `"./ports"`, `"./story-index"`, `"./assertions"`, `"./fixtures"`.
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### 1. Split helper modules
|
||||
|
||||
`storybook-helpers.ts` (monolith) → six focused modules under `src/`: `navigation`, `waiting`, `iframe`, `args`, `interactions`, `forms`, each with its own `.md`.
|
||||
|
||||
### 2. `STORYBOOK_PORTS` registry
|
||||
|
||||
`src/ports/registry.ts` — canonical `STORYBOOK_PORTS` map (package name → port number); `storyUrl(pkg, storyId)` helper replaces hard-coded `localhost:6001` across all consumer E2E suites.
|
||||
|
||||
### 3. Typed story-index discovery
|
||||
|
||||
`src/story-index/index.ts` — `loadStoryIndex(baseUrl)` fetches and parses Storybook `index.json`; `defineStoryId(kind, name)` generates a typed story ID.
|
||||
|
||||
### 4. Console/network/a11y assertion helpers
|
||||
|
||||
`src/assertions/console.ts` — `expectNoConsoleErrors(page, opts)`.
|
||||
`src/assertions/network.ts` — `expectNoFailedRequests(page)`, `expectAllFontsLoaded(page)`.
|
||||
`src/assertions/a11y.ts` — `auditAccessibility(page, opts)` running axe-core in the preview iframe; matches the CLAUDE.md web-app verification policy.
|
||||
|
||||
### 5. Reusable Playwright fixture
|
||||
|
||||
`src/fixtures/playwright.ts` — `test.extend` with a `storybook` fixture pre-configured for the port registry.
|
||||
|
||||
### 6. Tests (currently zero)
|
||||
|
||||
Write `src/**/*.test.ts` driving a local HTTP server with stub Storybook HTML; coverage ≥ 95%.
|
||||
|
||||
### 7. `@playwright/test` moved to `peerDependencies`
|
||||
|
||||
Removes the type-leak where public API signatures returned `Page`/`Locator`/`FrameLocator` but the package was declared dev-only.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
browser/dev/storybook-test-utils/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
navigation/
|
||||
navigation.ts
|
||||
navigation.test.ts
|
||||
navigation.md
|
||||
index.ts
|
||||
waiting/
|
||||
waiting.ts
|
||||
waiting.test.ts
|
||||
waiting.md
|
||||
index.ts
|
||||
iframe/
|
||||
iframe.ts
|
||||
iframe.test.ts
|
||||
iframe.md
|
||||
index.ts
|
||||
args/
|
||||
args.ts
|
||||
args.test.ts
|
||||
args.md
|
||||
index.ts
|
||||
interactions/
|
||||
interactions.ts
|
||||
interactions.test.ts
|
||||
interactions.md
|
||||
index.ts
|
||||
forms/
|
||||
forms.ts
|
||||
forms.test.ts
|
||||
forms.md
|
||||
index.ts
|
||||
ports/
|
||||
registry.ts
|
||||
registry.test.ts
|
||||
registry.md
|
||||
index.ts
|
||||
story-index/
|
||||
index-loader.ts
|
||||
index-loader.test.ts
|
||||
index-loader.md
|
||||
index.ts
|
||||
assertions/
|
||||
console.ts
|
||||
console.test.ts
|
||||
console.md
|
||||
network.ts
|
||||
network.test.ts
|
||||
network.md
|
||||
a11y.ts
|
||||
a11y.test.ts
|
||||
a11y.md
|
||||
index.ts
|
||||
fixtures/
|
||||
playwright.ts
|
||||
playwright.md
|
||||
index.ts
|
||||
types/
|
||||
types.ts
|
||||
types.md
|
||||
tests/
|
||||
integration.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: every `@suaveplan/*` package with Storybook stories and a `e2e/*.spec.ts` file.
|
||||
|
||||
**Breaking changes**: `@playwright/test` becomes `peerDependency`; consumers must have it in their own `devDependencies` (they typically already do).
|
||||
|
||||
**Prerequisites**: None — no runtime workspace deps beyond `tslib`.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Tasks: enterprise-browser-storybook-test-utils
|
||||
|
||||
**Change ID**: enterprise-browser-storybook-test-utils
|
||||
**Package**: `@suaveplan/storybook-test-utils`
|
||||
**Target path**: `browser/dev/storybook-test-utils`
|
||||
**Prerequisite packages on Verdaccio**: none (no workspace runtime deps)
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create directory tree per `proposal.md`.
|
||||
- [ ] `package.json` v0.0.0, `"sideEffects": false`.
|
||||
- `dependencies`: `@suaveplan/logger workspace:^`, `tslib catalog:build`
|
||||
- `peerDependencies`: `@playwright/test` (move from devDependencies)
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `@playwright/test catalog:testing`, `@suaveplan/testing workspace:^`
|
||||
- Subpath exports: `"."`, `"./navigation"`, `"./waiting"`, `"./iframe"`, `"./args"`, `"./interactions"`, `"./forms"`, `"./ports"`, `"./story-index"`, `"./assertions"`, `"./fixtures"`
|
||||
- [ ] `tsconfig.json`, `vitest.config.ts` (pool: forks).
|
||||
|
||||
---
|
||||
|
||||
## 2. Types (`src/types/types.ts`)
|
||||
|
||||
- [ ] `StoryMetadata`, `WaitOptions`, `UrlOptions`, `StoryIndex`, `StoryIndexEntry`, `AxeOptions`, `AxeResult`.
|
||||
- [ ] Write `types.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. `STORYBOOK_PORTS` registry (`src/ports/`)
|
||||
|
||||
- [ ] `registry.ts` — `STORYBOOK_PORTS: Record<string, number>` mapping package names to ports (react-ui=6001, react-audio=6004, react-auth=6010, r3f-camera=6019, etc.); `storyUrl(pkg, storyId, opts?)`.
|
||||
- [ ] Write `registry.test.ts` (lookup known port, unknown package throws, `storyUrl` formats correctly). Write `registry.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Navigation (`src/navigation/`)
|
||||
|
||||
- [ ] `navigation.ts` — `navigateToStory`, `navigateToStoryWithUI`, `switchToStory`, `reloadStory`.
|
||||
- [ ] Drive a stub Storybook HTTP server in tests (serve `iframe.html` + `/?path=/story/…` fixtures).
|
||||
- [ ] Write `navigation.test.ts`. Write `navigation.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Waiting (`src/waiting/`)
|
||||
|
||||
- [ ] `waiting.ts` — `waitForStoryLoaded`, `waitForStorybookIframe`, `waitForStoryInteractive`.
|
||||
- [ ] Handle SB7 vs SB8 iframe selector differences via auto-detection.
|
||||
- [ ] Write `waiting.test.ts`. Write `waiting.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Iframe access (`src/iframe/`)
|
||||
|
||||
- [ ] `iframe.ts` — `getStorybookFrame`, `getStoryCanvas`.
|
||||
- [ ] Write `iframe.test.ts`. Write `iframe.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Args (`src/args/`)
|
||||
|
||||
- [ ] `args.ts` — `getStoryArgs`, `updateStoryArgs<T>`.
|
||||
- [ ] Write `args.test.ts`. Write `args.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Interactions (`src/interactions/`)
|
||||
|
||||
- [ ] `interactions.ts` — `clickWithFallback`, `getClickableElement` (handles autoplay policies, strict-mode locator).
|
||||
- [ ] Write `interactions.test.ts`. Write `interactions.md`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Forms (`src/forms/`)
|
||||
|
||||
- [ ] `forms.ts` — `getFormFromStory`.
|
||||
- [ ] Write `forms.test.ts`. Write `forms.md`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Story-index discovery (`src/story-index/`)
|
||||
|
||||
- [ ] `index-loader.ts` — `loadStoryIndex(baseUrl)` fetches `index.json`; `defineStoryId(kind, name)` returns canonical ID.
|
||||
- [ ] Write `index-loader.test.ts` (stub server, parse, missing story throws). Write `index-loader.md`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Assertions (`src/assertions/`)
|
||||
|
||||
- [ ] `console.ts` — `expectNoConsoleErrors(page, opts?)` captures console logs during test and fails on unexpected errors.
|
||||
- [ ] `network.ts` — `expectNoFailedRequests(page)`, `expectAllFontsLoaded(page)`.
|
||||
- [ ] `a11y.ts` — `auditAccessibility(page, opts?)` runs axe-core in the story iframe; returns `AxeResult`.
|
||||
- [ ] Write tests for each (stub page + intercepted events). Write `.md` per module.
|
||||
|
||||
---
|
||||
|
||||
## 12. Playwright fixture (`src/fixtures/`)
|
||||
|
||||
- [ ] `playwright.ts` — `test.extend({ storybook: [storybookFixture, { scope: 'test' }] })` pre-configured with port registry.
|
||||
- [ ] Write `playwright.md`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Integration test (`tests/integration.test.ts`)
|
||||
|
||||
- [ ] Spin up an in-process HTTP server serving stub Storybook HTML.
|
||||
- [ ] Run `navigateToStory` → `waitForStoryLoaded` → `getStoryCanvas` → `updateStoryArgs` → `expectNoConsoleErrors`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Docs + verify
|
||||
|
||||
- [ ] Write `.md` per module (≥200 words, API reference, examples).
|
||||
- [ ] Write `README.md` with subpath table, port-registry guide, story-index guide, assertion guide.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/storybook-test-utils` — green.
|
||||
- [ ] `bunx turbo test:coverage` — 100% (currently 0%).
|
||||
- [ ] No `@ts-expect-error` in `src/`. `@playwright/test` declared as `peerDependency`.
|
||||
- [ ] `STORYBOOK_PORTS` registry replaces at least three consumer hard-coded ports.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/storybook-test-utils`.
|
||||
- [ ] Archive to `openspec/archive/2026-04-24-enterprise-browser-storybook-test-utils/`.
|
||||
Reference in New Issue
Block a user