openpencil/tests/AGENTS.md
Danila Poyarkov 7140328b1d
test(core): move the fig round-trip suite home, and stop test imports drilling
* test(core): move the fig round-trip suite into the package test home

The reader change added `verifier-contracts.test.ts` under
`tests/engine` and grew the migration baseline to admit it, which the
testing architecture forbids: new tests go to the canonical home and
the baseline only shrinks. It also started
`packages/core/tests/io/formats/fig/roundtrip/`, leaving two homes for
one domain.

These tests drive Core's exporter, so Core's home is the canonical one.
The directory moves whole — eleven files, its helpers and raw verifiers
with it — and picks up the local `assert`, `traversal`, `test-utils`
and fixture helpers already there. The baseline loses nine entries.

* refactor(core): address test helpers by alias instead of drilling

`open-pencil/no-deep-parent-relative-imports` already forbids `../../`
drilling, but `lint:structure` names the directories it checks and
`packages/{core,scene-graph,vue}/tests` were never added — so the 72
drilled imports in Core's tests, including the ones the round-trip move
just wrote, were never seen.

Core gains `#core-tests/*` beside fig's `#fig-tests/*`, registered with
the architecture rules, and its tests reach their helpers through it
and their source through `#core/*`. The three test directories join
`lint:structure`, which also surfaced six errors they had never been
checked for: four duplicated imports, an empty `noop`, and a
self-assignment the test meant, now written through a named binding.

Both guides state the rule, since a lint nobody can find in prose is
how this went unnoticed.

* fix(tools): point the heavy unit patterns at the relocated round-trip tests

`HEAVY_UNIT_TEST_PATTERNS` still listed the three heavy round-trip
tests under `tests/engine`. `test:unit` discovers them by directory so
the move looked clean, but `--heavy-only` selects by path and silently
stopped choosing them: 8 files, 52 tests, where it now runs 11 and 80.
2026-10-01 14:11:53 +04:00

5.2 KiB

Tests

packages/docs/development/testing.md is the canonical testing architecture: ownership table, fixture/driver/probe roles, browser and native boundaries, execution commands, server ownership, and worktree ports. This file holds the rules that bite during implementation.

Placement

  • Package-local tests mirror source domains under packages/<owner>/tests/; central app tests mirror src/app/** under tests/app/; tests/integration/ requires a genuinely cross-owner contract; E2E under tests/e2e/ follows user workflows; native and Figma acceptance are explicit exceptions.
  • Existing tests/engine/** domains migrate together with runner discovery: tools/dev/unit-tests/src/shards.ts lists each owner's canonical home and its current tests/engine directories, so a move is a git mv plus imports. bun run check:test-homes rejects any test added under tests/engine and any stale entry in tools/checks/test-homes/engine-baseline.txt; new tests go to the canonical home, and a moved file is removed from the baseline (--write regenerates it). Scene Graph has migrated to packages/scene-graph/tests and the editor domain to packages/core/tests/editor; put new Core tests there, mirroring packages/core/src.
  • Address a package's source by #<pkg>/* and its own helpers by #<pkg>-tests/* (#core-tests/*, #fig-tests/*); never drill with ../../. Registering a new alias means imports in the package manifest and PACKAGE_ALIASES/PACKAGE_ALIAS_OWNERS in tools/checks/architecture/src/steiger-rules/support.ts.
  • Owner-local helpers and fixtures stay local; only genuinely shared support goes under central tests/helpers/<domain>/ and tests/fixtures/. MCP transport tests: tests/engine/mcp/{server,stdio,transport} with tests/helpers/mcp.
  • Never commit temporary, diagnostic, or profile specs; keep them in ignored scratch/.

Writing specs

  • Test contracts and observable behavior, not source text. Specs use domain drivers and probes, not scattered Window/store traversal or unrestricted evaluator wrappers.
  • Locate behavior by accessible role and name, then label, then visible text. Scope repeated controls to a named region. Use scoped data-slot anatomy or semantic attributes (data-property, data-command, data-node-id) when needed; reserve data-test-id for integration boundaries and never add test-hook props or compound IDs.
  • Prefer test-runner-owned fixtures and request/route counters over browser globals. For in-page performance instrumentation, return a scoped JSHandle from evaluateHandle(), restore patched methods and listeners, and dispose the handle in finally; handles do not survive navigation. Assert transient DOM state with locators before the interaction ends.
  • Do not create a catch-all test Window interface or ad-hoc counter properties on window. Native-test declarations live in tests/helpers/tauri/native-global.d.ts; never expand production Window declarations for fixtures.
  • In Bun tests prefer injected dependencies or scoped spies with explicit cleanup. mock.restore() restores spies but does not undo mock.module() overrides; do not assume module mocks are isolated by cleanup hooks. Read the installed runner's lifecycle and mocking docs before adding global or module-level instrumentation.
  • Make flaky tests deterministic; raising a timeout is never the fix. Keep package-manager invocations out of bun test suites; their cold start is not bounded.

Browser runs

  • Use the canonical playwright.config.ts; do not create task-specific config copies or server runners. Playwright owns Vite; the Vite automation plugin owns MCP startup and cleanup; browser fixtures own interactions, not server processes.
  • Test scripts select their server; direct Playwright commands start both servers unless OPENPENCIL_TEST_SERVER=app|storybook|all is set. Managed runs start the intended checkout; server reuse is opt-in for local development only, never for baseline comparisons or CI. Isolate the app URL, MCP endpoint, CORS origin, socket, and discovery path together.
  • Pixel-affecting renderer changes need committed canvas snapshots (packages/core/AGENTS.md, Renderer). Update only the justified affected snapshot and rerun without update mode.

Native WebView

  • Native checks live under tests/e2e/native/** and run through WebdriverIO against an explicit test-only Tauri binary: bun run test:native builds and runs, bun run build:native-test only builds. The binary uses a separate application identifier, an ephemeral WebView data store, and process-memory credentials.
  • Never run UI smoke tests against production Keychain entries or clear user recovery data to unblock tests. Persistence across restarts needs a dedicated test-owned persistent profile.
  • Native tests answer only whether the real WebView and Tauri shell deliver an interaction; engine tests cover state contracts and Playwright covers app integration. Platform-limited checks skip rather than claim coverage. Synthetic composition does not prove IME behavior; native clipboard remains a separate acceptance gap without trusted OS clipboard events.
  • Centralize native invocation in a guarded helper using vendor-derived types; do not import packages inside serialized WebView callbacks or repeat direct Tauri-global access in specs.