commontoolsinc

pattern-test-to-integration

Convert Common Fabric pattern unit tests (`*.test.tsx` driven by actions and assertions) into browser integration tests (`packages/patterns/integration/*.test.ts`) that exercise the rendered UI, scale to a chosen size, and can be recorded with `deno task demo`. Use when asked to promote, mirror, spot-check, browser-test, or make a video demo from an existing pattern test, including multi-user pattern tests.

commontoolsinc 55 13 Updated 2mo ago

Resources

1
GitHub

Install

npx skillscat add commontoolsinc/labs/pattern-test-to-integration

Install via the SkillsCat registry.

SKILL.md

Pattern Test to Integration

Treat the pattern test as the behavioral story, not as code to transliterate.
Re-express its meaningful state transitions through the real shell, browser,
rendered controls, and visible outcomes.

Start from the current map

Read the source test, the pattern it instantiates, and these current references:

  • docs/common/ai/pattern-testing-guide.md — pattern-test semantics, including
    multi-user participants.
  • packages/patterns/deno.jsonc — the three test lanes and their CI/coverage
    roles.
  • docs/development/UI_TESTING.md — shadow DOM, semantic locators, presentation
    behavior, and browser interaction rules.
  • docs/development/TESTING.md — integration and deno task demo commands.
  • docs/development/waiting-in-tests.md — required reading before introducing
    any polling.
  • packages/patterns/integration/cfc-browser-helpers.ts — shared interaction,
    settling, effect-wait, and StepTimer seams.
  • packages/patterns/integration/pieces-controller.ts — the local
    PiecesController initializer with the shared compile-byte cache.

Use working integration tests as executable examples:

  • packages/patterns/integration/nested-counter.test.ts for a small
    single-browser interaction.
  • packages/patterns/integration/cfc-render-policy-demo.test.ts for a concise,
    captioned demo.
  • packages/patterns/integration/lunch-poll-vote.test.ts for two independent
    identities, browsers, and recordings on one shared timeline.

Prefer the nearest current test with the same topology over copying a generic
fixture. Tests are authoritative when a prose example has drifted.

Before choosing a destination, search packages/patterns/integration/ for the
pattern name, source path, and intended scenario. Extend current browser
coverage when it already owns the story. If a separate scenario is clearer, give
it a specific filename and selector; never overwrite or duplicate an existing
test merely because the unit test has the shorter name.

Preserve both test contracts

Keep the original colocated *.test.tsx. Pattern tests are the authored-pattern
coverage lane; a browser integration test does not replace that coverage.

Add the browser test under packages/patterns/integration/ as *.test.ts. The
new test covers boundaries the pattern test intentionally omits: shell
navigation, rendering, shadow DOM, real event dispatch, browser identity, and
cross-client propagation.

Do not call the conversion complete if the browser test still performs the
primary scenario by sending the same output streams and reading the same result
fields directly. Direct piece operations remain useful for setup, invisible
invariants, persistence checks, and diagnostics; they are not substitutes for
the user-visible action and outcome.

Translate intent across the boundary

Use this map as a starting point, then follow the actual UI:

Pattern-test construct Browser-test counterpart
Pattern({ inputs }) Build the program with resolveLocalProgram, then create a piece with PiecesController.create(..., { input, start: true })
instance.action.send(value) Fill, select, or click the rendered control that a user uses to cause that action
assert(() => instance.field === value) Wait for the corresponding rendered text, control state, list contents, or other observable DOM effect
{ label } / { await } in multiUserTest Coordinate real participant pages with event-driven waits on shared visible state
participant runtime A ShellIntegration connected to the shared piece, with identity reuse or separation matching the unit test's user/session topology

Preserve the scenario's causal ordering and important edge cases, but optimize
for a coherent browser story rather than one it block per unit assertion.
Group assertions that describe one visible state. An exhaustive internal
invariant with no UI manifestation should stay in the pattern test unless the
product is meant to expose it.

If the pattern has no rendered path for the behavior, say so explicitly. Add a
product UI seam only when the user's task authorizes changing the product; do
not create a test-only button or browser backdoor merely to force a conversion.

Use the real integration lifecycle

Build from the closest fixture, retaining the local invariants it demonstrates:

  • Bind every ShellIntegration lifecycle.

  • Generate explicit identities and keep all participants in the intended shared
    space. Reuse one identity across multiple shells when the source test models
    multiple sessions for the same user; use distinct identities for distinct
    users.

  • Build the program with resolveLocalProgram from
    @commonfabric/runner/local-program.deno, passing the harness's own
    resolve. Give it root when the source imports siblings outside its own
    directory, and dataFilePaths when the pattern reads an attached data file
    with dataFile():

    const program = await resolveLocalProgram(
      (resolver) => cc.runtime.harness.resolve(resolver),
      { main: sourcePath, root: rootPath, dataFilePaths: [dataPath] },
    );

    A pattern reading a data file compiles and type-checks without the attachment,
    so omitting it fails at the read rather than at the build.
    deno task check-local-program refuses a FileSystemProgramResolver built by
    hand, because a program assembled that way silently carries no data files.

  • Pass initial unit-test inputs through PiecesController.create's input
    option when they are part of the scenario.

  • Keep the result demanded with a result-cell sink when pull-mode reactivity
    requires it, and cancel every sink during cleanup.

  • Call ensureDefaultPattern() before browser navigation when the selected
    topology would otherwise race or cold-compile the space root; copy this seam
    only from a current fixture that explains why it is needed.

  • Navigate with ShellIntegration.goto(...) and the generated identity.

  • Dispose controllers and let bound shell lifecycles close browsers.

Use the helpers in packages/patterns/integration/cfc-browser-helpers.ts where
they already encode shadow traversal, one trusted interaction, view settling,
commit semantics, or an effect wait. Prefer accessible names and stable
product-facing selectors. Do not select by transient DOM structure when the UI
offers a semantic role, label, or explicit stable id.

Make the scenario's scale a knob

A browser test written at one fixed size answers whether the feature works. The
same test with its size parameterized also answers how the feature behaves as
data grows, which is a different and usually more valuable question — and it
costs one constant to keep both. Read the size from the environment with a small
default, so CI keeps paying the small number while an investigation can ask for
a large one.

Three distinctions are worth building in, because retrofitting them later means
rewriting the scenario:

  • Composed versus seeded. A couple of items driven through the real composer
    are what make the test a regression test; the rest exist only to establish
    size and should be created through the verb. Keep the two counts separate and
    derive the seeded count from the total. Presentation mode animates every
    keystroke, so a large board built through the composer is also an unwatchable
    recording.
  • Where the bulk is created. Seeding before the view is ever opened and
    seeding while it is on screen measure different things — the first is the cost
    with nothing rendering, the second is the cost of the live view keeping up.
    Make that a knob too rather than picking one.
  • Extra timed operations after the bulk exists. What a regression actually
    cares about is what one more costs on a board of that size, which is not
    recoverable by dividing the time it took to build it.

Emit measurements as parseable single lines carrying the size they were taken
at, rather than only as step durations, so a sweep across sizes can be collected
without re-reading logs by eye.

packages/patterns/integration/topic-board-scale.bench.ts is the same idea in
the benchmark lane, including how it declares sizes it cannot yet run. When the
task is measurement rather than conversion, skills/perf-investigation/SKILL.md
picks up from here.

Keep correctness timing separate from presentation

Wait for a real state or DOM effect. Do not add sleep, waitForTimeout, or a
larger correctness timeout to make the video readable. Read
docs/development/waiting-in-tests.md before adding a bounded poll, and explain
why no event-driven observation exists if one is still necessary.

Make demo pacing an annotation of the same test:

  • Give each shell stable presentation metadata; use distinct labels and colors
    for multiple participants.
  • Wrap viewer-meaningful scenario boundaries in StepTimer.run(label, action).
    Write labels for the viewer: describe what changes without leaking hidden or
    protected values before the UI reveals them.
  • Keep using the existing click, fill, keyboard, and element paths. Presentation
    mode observes those paths; do not invent demo.click() or demo.type().
  • Preserve one real browser per participant. The demo compositor owns the
    side-by-side or grid layout.

The demo command records the complete selected *.test.ts file, including all
of its it blocks. Keep the file's ordered scenario meaningful as a whole. Do
not silently select ignored or environment-gated tests as default demos.

Verify the paired result

Run the original pattern test before conversion to establish a clean baseline,
and again after any source/interface change:

deno task cf test packages/patterns/<pattern>/<name>.test.tsx

If the test imports sibling modules above its own directory, pass their common
source root just as the CLI error suggests, for example:

deno task cf test packages/patterns/<pattern>/<name>.test.tsx --root packages/patterns

Run the browser test through the integration selector, using its exact filename
stem:

deno task integration patterns <integration-test-name>

When demo output is part of the request and FFmpeg is available, record the same
test rather than a demo-only variant:

deno task demo patterns <integration-test-name>

Inspect the video or gallery when presentation quality matters. Passing tests
prove correctness; they do not prove that labels, pacing, framing, and
multi-user composition are understandable to a viewer.