Agent Skills › iamcheyan/oc › component-tests

component-tests

GitHub

用于编写、调试和审查基于 Storybook 的 Playwright 组件测试。涵盖测试文件结构、挂载参数配置、共享服务状态及常见 CI 失败场景处理,确保前端组件测试的稳定性和正确性。

.opencode/skills/component-tests/SKILL.md iamcheyan/oc

触发场景

编写或修改 Storybook 组件测试 调试失败的组件测试 审查前端 UI 测试代码

安装

npx skills add iamcheyan/oc --skill component-tests -g -y
更多选项

非标准路径

npx skills add https://github.com/iamcheyan/oc/tree/main/.opencode/skills/component-tests -g -y

不安装直接使用

npx skills use iamcheyan/oc@component-tests

指定 Agent (Claude Code)

npx skills add iamcheyan/oc --skill component-tests -a claude-code -g -y

安装 repo 全部 skill

npx skills add iamcheyan/oc --all -g -y

预览 repo 内 skill

npx skills add iamcheyan/oc --list

SKILL.md

Frontmatter
{
    "name": "component-tests",
    "description": "Use when writing, changing, debugging, or reviewing Storybook-based Playwright component tests. That covers specs in packages\/app\/component-tests or packages\/session-ui\/component-tests, `*.fixture.tsx` files there or beside source (such as packages\/gui-extensions\/src\/browser\/panel.fixture.tsx), the stories they mount, and the shared harness in packages\/storybook (playwright\/config.ts, playwright\/story.ts, .storybook\/main.ts, preview.tsx, mocks). Also use when a component test is flaky, passed on retry, or fails with \"Execution context was destroyed\", \"Failed to fetch dynamically imported module\", or `#storybook-root` hidden. Covers mount args, source modules imported by URL, Vite dependency pre-bundling and reloads, cold vs warm Storybook, mobile drawers, geometry, and checks before pushing."
}

Component tests

Each package suite starts one Storybook/Vite dev server, and every worker shares it (packages/storybook/playwright/config.ts:17-24; CI uses 2 workers, config.ts:15). With fullyParallel (config.ts:12), the tests of one file run on several workers in any order. There is no backend: server, SDK and sync modules are in-page mocks. Every test gets a fresh context, and mount is a full iframe navigation (packages/storybook/playwright/story.ts:37). Slow or flaky component tests in CI have come from five sources: the shared server's dependency optimizer, module resolution that depended on the install layout, a cold first story, a product race (the mobile drawer), or an action that ran before the UI reached the state it needed.

Read these first rather than repeating them:

  • packages/storybook/playwright/README.md covers layout, run commands and "Adding a test".
  • packages/app/AGENTS.md "Tests" decides whether a test should exist, when theme or direction cases are allowed, and that a regression test must fail before the fix.
  • packages/app/e2e/AGENTS.md "Test Hygiene" applies here unchanged.

Stories and mount

  • Location. Put the story next to the production component under src/ of ui, session-ui, app or gui-extensions. Storybook indexes only the stories globs in packages/storybook/.storybook/main.ts:44-49.
  • Story ID. It is the meta id if one is set (packages/session-ui/src/components/session-review.stories.tsx:25). Otherwise it is the kebab-cased title, then --, then the kebab-cased export. Check it at http://127.0.0.1:6006/?path=/story/<id>. A wrong ID never renders, so mount waits out its full 30 s root wait (story.ts:39) and then fails.
  • URL args can be dropped. mount(id, { args, globals }) writes them into the URL (story.ts:20-36). Storybook drops an arg when the story declares no args/argTypes entry for its key. It also drops one whose value is not [A-Za-z0-9 _-]*, a number, a hex color or rgb()/hsl(), which rules out paths, URLs and file names with dots. The only trace is the browser console warning "Omitted potentially unsafe URL args.", and the test then passes on the default scenario.
    • Give every arg a typed default. With args: { split: false } (session-review.stories.tsx:129), split:false arrives as a boolean.
    • Pick punctuated values inside the story, from a scenario-style arg.
  • Globals are theme, direction and locale (packages/storybook/.storybook/preview.tsx:106-122). mount also sets a11y.manual (story.ts:29-36); caller globals override it.
  • Portaled UI. mount returns #storybook-root. Dialogs, menus and drawers render outside it, so locate them from page (packages/app/component-tests/composer.spec.ts:101-107).
  • Server state comes from the resolve.alias list in viteFinal (main.ts:72-94), which points app server, SDK and sync modules at packages/storybook/.storybook/mocks/app/**. Extend a mock rather than adding a server. Restore mock state in onCleanup (mockProviderAuth, packages/app/src/providers/connect/dialog.stories.tsx:23) so that switching stories in the Storybook UI does not inherit it. Tests reset it on every mount.
  • Requests the mocks do not answer. Route them with page.route. That includes server endpoints such as /api/fs/read (packages/session-ui/component-tests/read-image.spec.ts:20) and external URLs such as favicons (packages/session-ui/component-tests/markdown.spec.ts:217).

Fixtures imported by URL

Use these when the contract is not visible in a story, such as construction counts, unmount races or cache state.

  • Fixture file. Put a *.fixture.tsx file in component-tests/ or beside the source it drives (packages/gui-extensions/src/browser/panel.fixture.tsx). It exports a mountX() that renders into a host with a data-testid (packages/session-ui/component-tests/basic-tool.fixture.tsx:7-46). Never end the name in .spec or .test.

  • Mount it in three steps.

    1. Mount a light story first, so that the app styles load.
    2. Import the fixture and call it inside page.evaluate.
    3. Locate it with page.getByTestId (packages/session-ui/component-tests/basic-tool.spec.ts:30-36).

    It renders outside the preview decorators, so it must supply its own providers (packages/app/component-tests/browser-pane-restore.spec.ts:69-83).

  • Module URLs. Build each one with sourceURL(new URL("<relative path with extension>", import.meta.url)) (story.ts:10), written as a string literal in the spec (browser-pane-restore.spec.ts:6-24). Any other spelling either fails to load or loads a second module instance whose contexts the page never provides; a hand-written /@fs//... is the documented second-instance case (story.ts:6-9). The literal also matters for pre-bundling (next section).

  • Aliases. A module imported by URL is the real file. App code that reaches the same module through a main.ts alias gets the mock (browser-pane-restore.spec.ts:95-96). Decide which of the two the contract needs.

  • Spec imports. Import source modules into the spec with import type only (browser-pane-restore.spec.ts:2-4). A value import runs browser code in the Node runner.

  • Async order. Let the test decide the order through deferreds the fixture exposes (mountExtensionHost, packages/app/component-tests/extension-host.fixture.tsx:135). Await the outcome they lead to (extension-host.spec.ts:46-77), or poll in-page state with until (extension-host.fixture.tsx:124-132). Do not copy the fixed in-page setTimeout waits that other cases in extension-host.spec.ts (:38, :88-90, :108-119) use to prove a negative.

Vite pre-bundling and reloads

viteFinal drops storybook:optimize-deps-plugin (main.ts:105-108) and sets optimizeDeps itself (main.ts:56-67). Its entries are every *.stories.tsx and *.worker.ts under the four src/ trees, every module a component spec names by a ./ or ../ string literal ending in .ts/.tsx (collected at main.ts:15-29), and preview.tsx; include adds the Storybook preview runtime. Vite pre-bundles their bare imports before the first page loads. A dependency the scan misses is found on first import instead. If that changes chunks already served, Vite logs ✨ optimized dependencies changed. reloading and sends full-reload with path * to every page in every worker, and requests with a stale ?v= hash get a 504 "Outdated Optimize Dep".

  • The bar is zero reloads. A cold run of either suite prints neither new dependencies optimized nor optimized dependencies changed; neither did the Linux and Windows CI legs. Dropping preview.tsx and include from main.ts brings one reload back, so these lines are the signal to watch.
  • Keep new code reachable by the scan. A module path built at runtime, in a template literal, or without its extension is not collected, and neither is a spec named other than *.spec.ts. Name stories *.stories.tsx: Storybook indexes other extensions, but the scan reads only that one. Name workers *.worker.ts, since the scan does not follow new Worker(new URL(…)). Add an optimizeDeps.include entry only for a dependency the scan cannot reach. Vite names it in ✨ new dependencies optimized: …, and DEBUG=vite:deps replaces that line with scan and discovery detail.
  • Symptoms. "page.evaluate: Execution context was destroyed", "Failed to fetch dynamically imported module …/@fs/…", or fixture state that is gone right after mount ("Registrations: 1" not found, an empty tab strip).
  • Only mount() survives a reload, because it navigates back to the same URL. A reload loses everything set up after it: page.evaluate imports, mounted fixtures, state from earlier clicks, and the elements your locators pointed at. Another worker's discovery can trigger it at any moment.
  • Fix reloads in the scan, never with retries, timeouts or import order. The import-everything-before-mount Promise.all in browser-pane-restore.spec.ts:43-58 (comment at :56) only narrowed the window. Before the entries existed, that file's :123 and :150 were retried in 31 CI jobs each, and failed attempts cost about 39-47 s (Linux) and 34 s (Windows) of worker time per run.
  • Clear the cache whenever entries matter. Vite's getConfigHash covers optimizeDeps.include, resolve and plugin names, but not entries, and a valid _metadata.json skips the scan. A warm cache therefore never scans a new story or fixture, so it shows neither the reloads nor the absence of reloads that CI's cold scan would. Delete packages/storybook/node_modules/.cache before judging.
  • Resolve through package resolution, not paths into node_modules. main.ts:69-71 dedupes @tanstack/solid-query instead of aliasing it into app/node_modules, a path that exists only under bun's isolated linker. While Windows CI installed hoisted, that alias made every app story fail there, and 23 of 30 runs hit the step's 15-minute cap. CI now runs the same bun install --frozen-lockfile on every OS (.github/actions/setup-bun/action.yml:66-68), so such an alias would pass CI and still break a hoisted local install.

Cold vs warm, and what a mount costs

  • CI is always cold. Each suite runs in its own leg on a fresh runner (test.yml:228-229), so every leg starts Storybook with no cache and runs the full scan.
  • Do not count the first test. In one CI run of this harness, the first test per worker took 5.7-7.0 s on Linux and 14-15 s on Windows. Warm tests had a median of 1.5 s (app) and 2.0 s (session-ui) on Linux and 2.4-2.6 s on Windows. A warm mount (navigation plus root wait) had a median of 1.0 s (app) and 1.5 s (session-ui) on Linux and 1.7-1.9 s on Windows, which is most of a typical test. Judge cost by warm attempts.
  • Local reuse. Playwright reuses whatever answers on the port (config.ts:20; default 6006, config.ts:3). That hides cold cost and reloads, and with several worktrees another checkout's Storybook can serve your tests its own code. Set PLAYWRIGHT_STORYBOOK_PORT or PLAYWRIGHT_STORYBOOK_URL (config.ts:3-4). On Windows pick a port below 49152: the dynamic range starts there, and Hyper-V and WSL reserve blocks inside it (netsh int ipv4 show excludedportrange protocol=tcp). Two Storybooks in one checkout share, and race on, packages/storybook/node_modules/.cache/storybook/<version>/<hash>/sb-vite, so a separate port is not enough: use a separate worktree.
  • Keep mounts few. Put one scenario in each test, and resize in place with page.setViewportSize (composer.spec.ts:28-29) instead of mounting again.
    • a11y.manual makes addon-a11y skip its axe run after each render, so changing a11y.test in preview.tsx:133-135 is not enforced by these tests. Assert accessibility in the spec when it is the contract.
    • CI records video only on the first retry, next to the trace; local runs keep retain-on-failure (config.ts:27-30).

Mobile, touch and drawers

  • Touch. Use story.use({ hasTouch: true, isMobile: true, viewport: { width: 390, height: 844 } }) inside a story.describe (packages/session-ui/component-tests/session-timeline-mobile.spec.ts:3-4). These options are test-scoped, so the shared workers stay. Prove that emulation took effect with matchMedia("(hover: none)") (composer.spec.ts:244), and interact with tap() or page.touchscreen.tap (session-timeline-mobile.spec.ts:41-42).
  • No launchOptions. They are worker-scoped, so the file gets its own worker hash and its own workers. App e2e had to move packages/app/e2e/regression/mobile-timeline-scroll.spec.ts:18 into its own fullyParallel project (packages/app/playwright.config.ts:69-86) because it otherwise formed one long serial group running alone on a single worker.
  • Drawers. Below 768 px, pickers open in a bottom drawer (composer.spec.ts:100-107).
    • @corvu/drawer 0.2.4 lost a close requested in the first frames after opening: focus stayed trapped in an off-screen sheet and body kept pointer-events: none. Before the fix, this caused most first-attempt failures of composer.spec.ts:5, in 17 of 31 Linux runs.
    • patches/@corvu%2Fdrawer@0.2.4.patch (root package.json patchedDependencies) fixes it, so tests need no wait before pressing Escape or tapping outside.
    • composer.spec.ts:375-429 guards the patch. It dismisses in-page, before and as data-opening appears, because Playwright input cannot target those frames. Upgrading @corvu/drawer drops the patch, so rerun those tests.
    • Before measuring or screenshotting a drawer, wait until it is fully in the viewport and has no data-transitioning (packages/app/e2e/regression/mobile-model-pickers.spec.ts:25-26).
    • A stuck overlay is a product bug. Fix the product and guard it in-page, as the patch did, rather than waiting it out.

Geometry, platforms, known failures

  • Fonts. Inter's @font-face has no font-display (packages/app/src/index.css:13-18), so a story can lay out with fallback metrics and then shift. Windows CI saw 6.5 px in the composer overflow measurement (composer.spec.ts:93-95), which now waits first (:25-26). Before measuring, run await page.evaluate(() => document.fonts.ready.then(() => undefined)) (markdown.spec.ts:247). The .then keeps the FontFaceSet from being returned to Node.
  • Page scroll. Outside fullscreen, the preview frame is calc(100vh - 2rem) to fit Storybook's 1rem padding (preview.tsx:82-84). With 100vh, every such story scrolled by 32 px, and any scroll-into-view shifted boxes (composer.spec.ts:116 saw y go from 300 to 268). Do not give a story root 100vh. A story taller than the viewport still scrolls, so measure relative to a container captured at the same moment, or use parameters: { layout: "fullscreen" }.
  • Shortcuts. Write them as ControlOrMeta+… (browser-pane-restore.spec.ts:134). Derive expected labels from navigator.platform (packages/app/component-tests/session-question-dock.spec.ts:121).
  • Known local failure. On macOS, browser-pane-restore.spec.ts:123 fails after ControlOrMeta+w (Meta+W, :134-135), while CI (Control+W) passes. It is not your regression.
  • story.fail. It waits out the full 10 s expect timeout on every run: packages/session-ui/component-tests/file-tool.spec.ts:49-50 took 12.6 s on Linux CI and 14.8 s on Windows CI. Fix the product instead, or make the expected failure an immediate check once the state has settled.

Before you push

From the owning package (packages/session-ui or packages/app):

  1. Repeat at CI's worker count. Locally retries are already 0 (config.ts:14). Every attempt must pass: bun run test:components -- component-tests/<file>.spec.ts --workers=2 --repeat-each=10.

  2. Read durations in bunx playwright show-report component-tests/playwright-report. Compare warm tests with the CI medians above. Anything much slower needs a reason in the work the test does.

  3. Rehearse load. Late font swaps and focus restored after a menu closes (composer.spec.ts:114-115) race the test under load. In a throwaway copy of the spec, throttle before mount with const cdp = await page.context().newCDPSession(page) and then await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 }). Alternatively, repeat while the other suite runs.

  4. Run each suite cold whenever you add a story, fixture, dependency, worker or lazy import, or change packages/storybook. From the repo root, once per suite, as CI runs each in its own leg. Playwright pipes Storybook's output into its own (config.ts:22-23), and a free port makes it start a fresh server:

    rm -rf packages/storybook/node_modules/.cache
    PLAYWRIGHT_STORYBOOK_PORT=6216 bun run --cwd packages/app test:components -- --workers=2 2>&1 | tee /tmp/app-components.log
    grep -E "new dependencies optimized|optimized dependencies changed" /tmp/app-components.log
    

    The grep must print nothing. Count optimizer events, not durations: under load, the same config ran anywhere from 127 to 189 s.

  5. Windows. Check platform keys and labels and font-dependent geometry. The Windows component legs are the authoritative check.

    • Do not skip on Windows, and do not raise config.ts timeouts, retries or the mount wait.
    • If one test does genuinely expensive work, call story.setTimeout(...) in that test, with a comment that names the work.
  6. Confirm CI ran the component legs. E2E never runs on pushes to dev (test.yml:28), so no post-merge run catches a regression.

    • The affected gate filters on @opencode/app and @opencode/storybook (test.yml:52-53). ui, session-ui and gui-extensions count through app's dependencies. workflow_dispatch forces the legs on (test.yml:48-51).
    • test.yml is listed in globalDependencies (turbo.json:8), so editing it marks every package affected.
    • The required e2e (linux) and e2e (windows) checks also pass when no leg was needed (test.yml:316-335). Look for e2e (<os>, session-ui-components) and e2e (<os>, app-components).
    • If CI skipped them, report your local cold run in the PR.

When CI flakes

  • Download the artifact. Run gh run download <run-id> -n playwright-<linux|windows>-<session-ui|app>-components-<attempt> -D <dir> (test.yml:301-314). Results are in <dir>/session-ui/component-tests/ or <dir>/app/component-tests/. Only the first retry has a trace and, in CI, a video: test-results/<test>-components-retry1/.
  • Find reloads. Storybook's output reaches the job log with a [WebServer] prefix, so grep the log for optimized dependencies changed. In the retry trace, a reload shows up as any of:
    • a second iframe.html load and a second [vite] connecting...;
    • the deps ?v= hash changing between requests;
    • 504 "Outdated Optimize Dep" on deps/*.js.

版本历史

  • d9b4ce8 当前 2026-10-18 23:22

同 Skill 集合

.opencode/skills/opencode-dev/SKILL.md
.opencode/skills/rtl-aware-development/SKILL.md
.opencode/skills/sample-skill/SKILL.md
packages/opencode-vim/skills/git-master/SKILL.md
packages/opencode-vim/skills/git-workflow/SKILL.md
packages/plugins/figma/skills/figma-code-connect/SKILL.md
packages/plugins/figma/skills/figma-create-new-file/SKILL.md
packages/plugins/figma/skills/figma-generative-plugins/SKILL.md
packages/plugins/figma/skills/figma-implement-motion/SKILL.md
packages/plugins/figma/skills/figma-shaders/SKILL.md
packages/plugins/figma/skills/figma-use-figjam/SKILL.md
packages/plugins/figma/skills/figma-use-motion/SKILL.md
packages/plugins/figma/skills/figma-use-slides/SKILL.md
.opencode/skills/app-e2e-tests/SKILL.md
.opencode/skills/effect/SKILL.md
.opencode/skills/tui-tests/SKILL.md
.opencode/skills/unit-tests/SKILL.md
packages/plugins/figma/skills/figma-design-to-code/SKILL.md
packages/plugins/figma/skills/figma-generate-design/SKILL.md
packages/plugins/figma/skills/figma-generate-diagram/SKILL.md
packages/plugins/figma/skills/figma-generate-library/SKILL.md
packages/plugins/figma/skills/figma-swiftui/SKILL.md
packages/plugins/figma/skills/figma-use/SKILL.md

元信息

文件数
0
版本
d9b4ce8
Hash
f6efe004
收录时间
2026-10-18 23:22

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-20 19:57
浙ICP备14020137号-1