opencode/packages/storybook/playwright
opencode-agent[bot] 1aa4046f02
test(app): isolate component coverage with storybook (#45142)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-27 02:42:27 +08:00
..
config.ts
README.md
story.ts

Component browser tests

Production Solid components are tested through their existing Storybook stories without booting the app, configuring a server, seeding browser storage, or navigating unrelated routes.

Keep each spec in the package that owns its production component:

  • packages/session-ui/component-tests/ owns timeline, tool, notice, reasoning, lifecycle, and review coverage.
  • packages/app/component-tests/ owns Composer and other app-only component coverage.
  • packages/storybook/playwright/ owns the shared Storybook startup configuration and story mount fixture.

Run a package's isolated browser suite from that package:

# Session UI components.
cd packages/session-ui
bun run test:components
bun run test:components -- component-tests/session-timeline.spec.ts
bun run test:components:ui

# App-owned components.
cd packages/app
bun run test:components
bun run test:components -- component-tests/composer.spec.ts

Both suites are separately filterable Turbo tasks:

bun turbo test:components --filter=@opencode-ai/session-ui
bun turbo test:components --filter=@opencode-ai/app

Component browser coverage deliberately remains separate from each package's default test script and from packages/app's test:e2e, so expensive Storybook checks can be scheduled independently from required unit and full-app journey CI. Set PLAYWRIGHT_STORYBOOK_URL to reuse an existing Storybook instance or PLAYWRIGHT_STORYBOOK_PORT to choose its port.

Adding a test

Keep inspectable scenarios next to the production component in a *.stories.tsx file. A story owns its fixtures, providers, state, and callbacks; its package-local spec owns user-visible interactions and assertions.

import { expect, story } from "../../storybook/playwright/story"

// Moved from packages/app/e2e/regression/session-timeline-context-state.spec.ts
story("preserves collapsed state while a tool completes", async ({ mount }) => {
  const component = await mount("current-session-research-agents--agent-research", {
    args: { scenario: "exploration" },
  })
  const trigger = component.locator('[data-slot="collapsible-trigger"]')

  await expect(trigger).toHaveAttribute("aria-expanded", "false")
  await component.getByRole("button", { name: "Complete read" }).click()
  await expect(trigger).toHaveAttribute("aria-expanded", "false")
})

The story ID is the Storybook component ID followed by -- and the kebab-cased story export. Open the same story in Storybook to inspect exactly the scenario covered by the browser test. Preserve an original-source-path comment for every migrated E2E case.

Keep cross-route navigation, remote-server ownership, persistent session state, full-app virtualization, and workflows spanning independent surfaces in packages/app/e2e/.

Component rendering and integration coverage can be complementary. A local story control that installs a completed message does not test event delivery, production reducer cleanup, or a live stream. Keep those original checks in E2E, including stream/chunk identity, compaction and retry events, independent lifecycle transitions, and the real app scroll owner. A provenance comment records the source of a component assertion; it is not evidence that its integration counterpart can be deleted.

When moving an assertion, preserve its discriminating fixture: file status kinds, empty/single-variant inputs, singleton groups, live message state, and the order of intermediate updates. Verify the actual scroll container overflows before asserting that keyboard activation does not scroll it.