writing-tests
Skill285 starsChanged 15 days agoMITView on GitHub
How to write tests in the Portable Text Editor monorepo. Use whenever writing or editing tests in PTE packages. Covers test placement, harnesses, deterministic fixtures, and the house assertion style (no indirection, full values with toEqual).
39 other projects use a version of this.
Adopted by
Most recently taken up by
- scheron/Dailytheir own version, first seen Nov 8, 2026
- mcp-use/mcp-usetheir own version, first seen Nov 8, 2026
- yuri-xyz/skillstheir own version, first seen Nov 8, 2026
- lox/agent-skillstheir own version, first seen Nov 7, 2026
- JetBrains/intellij-communitytheir own version, first seen Nov 7, 2026
- iliaal/ai-skillstheir own version, first seen Nov 7, 2026
Used with
Projects with it also use reviewing-code, running-tests and 4 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
bostonaholicSkillreviewing-code
Code review methodology for evaluating implementation changes. Use when reviewing code changes for quality, design, correctness, and maintainability. Focuses on changes made during implementation using Conventional Comments for clear, actionable feedback.
Also in 4 projects that use this
OpenAISkillrunning-tests
Guide for running tests in the openai-dotnet repository. Use this when asked to run, debug, or validate tests, or when writing new tests. Explains test modes (Playback, Record, Live), how to identify recorded vs non-recorded tests, environment variable configuration, and what to do when recordings are missing or stale.
Also in 3 projects that use this
BlockSkilldebugging
Debugging guidelines and best practices
Also in 5 projects that use this
VercelSkilltechnical-writing
Write, edit, review, or audit user-facing documentation for the eve repository. Use for changes under docs/, documentation tied to eve APIs or CLI behavior, docs work based on Slack or support feedback, and requests to make eve docs clearer, more natural, or less AI-patterned while verifying claims against current source, tests, CLI help, public releases, and repository conventions.
Also in 3 projects that use this
SentrySkillsecurity-review
Finds exploitable application security vulnerabilities in code changes. Use for Warden security scans, appsec review, OWASP-style checks, authentication or authorization bugs, injection, XSS, SSRF, path traversal, secrets, unsafe crypto, webhook verification, open redirects, or sensitive data exposure.
Also in 3 projects that use this
GoogleSkillimprove-codebase-architecture
Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
Also in 3 projects that use this
Other versions39 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 39
- remotion-dev/remotion★ 72ksame name
- PostHog/posthog★ 40ksame name
- JetBrains/intellij-community★ 21ksame name
- quarkusio/quarkus★ 16ksame name
- mcp-use/mcp-use★ 11ksame name
- DetachHead/rebased★ 6.5ksame name
- chattocorp/chatto★ 2.9ksame name
- starkware-libs/cairo★ 1.9ksame name
- bholmesdev/hubble.md★ 1.5ksame name
- amplifthq/openmeld★ 1.4ksame name
29 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- backportingSkill
- changesetsSkill
- code-commentsSkill
- commitsSkill
- next-branchSkill
- pr-descriptionsSkill
- reviewing-prsSkill
- product-copy-assistantSkill
- portable-text-editorSkill
The file
We split this skill into the rules it gives an agent
10 rules. 0 are common, 4 are distinctive, and 6 are very specific to this project.
0
Common
Most of the 39 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 19
When scaffolding a new package, mirror plugin-sdk-value's vitest config: a browser project including src//*.test.tsx and a unit project …
- Very specific: line 25
Keys: always createTestKeyGenerator from @portabletext/test.
- Very specific: line 32
Wait for content to render before focusing or selecting in it.
- Very specific: line 34
Every DOM query goes inside the retried callback.
- Very specific: line 46
Do not build summarizer helpers (string transcripts, custom matchers, mapping functions) between the collected data and the assertion.
- Distinctive: line 52
Prefer the full emitted payload sequence over counting callback calls.
- Distinctive: line 59
Test files hold comments to the same bar as source files: the default is none.
- Very specific: line 61
No bare test.skip. Make the race deterministic, or pin a reproducible bug with test.fails.
- Distinctive: line 67
When test code duplicates production logic from elsewhere (a copied helper in a plugin), port the source's test suite wholesale as the …
- Distinctive: line 73
When replacing local fixtures with a shared helper, the built values must stay deep-equal.
“Common” is measured: kept by at least 6 in 10 of the 39 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing PTE tests
Test placement
Before writing any test, find its existing home. Always search for an established suite covering the domain (ls tests/ | grep -i <topic>, rg -l <mechanism> tests/) and read its scenario list before creating a file. The canonical suites are broad (behavior-api.test.tsx carries 30+ Behavior contract scenarios); a contract pin belongs next to its siblings, where reviewers and future failures will look for it. A new file is the exception, for a genuinely new domain, not the default. Also check whether the contract is already pinned in a different form before adding it at all.
The file extension is the discriminator, never a .browser suffix:
*.test.tsxruns in the browser project (playwright: chromium, firefox, webkit)*.test.tsruns in the unit project (node)*.test-d.tsis type-level, usingexpectTypeOffrom vitestgherkin-tests/*.feature+ racejar for behavior specs (Feature({featureText, stepDefinitions, parameterTypes}))
When scaffolding a new package, mirror plugin-sdk-value's vitest config: a browser project including src/**/*.test.tsx and a unit project including src/**/*.test.ts.
Harnesses
- Editor integration tests:
createTestEditorfrom@portabletext/editor/test/vitest(inside the editor package:../src/test/vitest). Returns{editor, locator}. Plugins and probes go in aschildren, but note they mount as siblings afterPortableTextEditable; a provider that must wrap the editable needs a hand-rolled render with a comment explaining why. - Selector/pure-function tests:
createTestSnapshot(in the editor package:test-utils/create-test-snapshot; in plugins: duplicate it locally on public types, it is ~30 lines). - Keys: always
createTestKeyGeneratorfrom@portabletext/test. It is deterministic (k0,k1, ...), which is what makes full-literal assertions possible. Generated keys may be asserted literally.
Triggers
- Real user interaction via
userEvent(userEvent.type(locator, 'foo')). Typing with no caret set lands at the block start. - Everything else via
editor.send({type: ...})with full payloads. This includes native-shaped events:drag.dragover,drag.dropetc. accept completeposition/dragOrigin/originEventobjects, so no DOM event simulation is needed to test a behavior. - The exception is a test of the editable's own handling of a native event that
userEventcannot produce, such as abeforeinputinput type keyboard input never fires. Set a real DOM selection and dispatch the real event on the editable (new InputEvent('beforeinput', {inputType, bubbles: true, cancelable: true})). - Wait for content to render before focusing or selecting in it. A
focusorselectsent straight after inserting blocks can land before the DOM exists and fail silently. - Async settling via
vi.waitForaround the assertion that proves the trigger landed. Debounced channels (the mutation batcher) need their ownvi.waitFor; comparing them synchronously after another assertion passes is a race. - Every DOM query goes inside the retried callback. A
querySelectorrun once beforevi.waitForis never re-run, so the wait checks the same stale element ornullon every retry. - A lone locator assertion is awaited directly:
await expect.element(locator).toBeVisible(), not wrapped invi.waitFor.expect.elementalready retries until its own timeout, and the wrapper cuts that tovi.waitFor's 1s default. Pass a longer timeout toexpect.elementitself (expect.element(locator, {timeout: 10_000})). - Never sleep (
setTimeout, arbitrary delays), even for negative asserts. To prove "nothing was emitted", create a deterministic flush point: perform a local edit through the same ordered channel and assert the full emission list contains exactly that edit's patches, a would-be echo would have to surface no later than the edit's own emission. This also upgrades the negative assert to a full-value pin. The only exception is a wait with no observable signal to anchor on, such as a real IME sequence driven through CDP, and it carries a comment naming the missing signal. - Simulated races are event-driven, never clock-driven. To reconstruct a stale-echo or interleaving scenario, capture the payload at one observable anchor (
vi.waitForon the emission that carries it) and deliver it after a later observable anchor (the subsequent flush landing in the collected events). Staleness is then a property of which value arrives, independent of runner speed. A fixed-delay reconstruction is flaky by construction on slow runners, even when its premise check fails in the safe direction: the CI failure is honest but intermittent, which is still a broken test. - To order async responses (a stale result arriving after a newer one), use promises the test resolves itself, then assert once the resolution is observable.
- A timer boundary (a debounce or throttle window) is the one thing to test on fake timers, with explicit
vi.advanceTimersByTime. Never mix real waits and fake timers in one test, and note thatvi.waitForandexpect.polladvance fake time while fake timers are on. - Contract tests never assert on wall-clock duration or log timings. Pin complexity structurally (call counts, render counts) and keep any genuinely needed time ceiling in its own clearly named test.
- The timed tests in
packages/editor/tests/performance.test.tsxare benchmarks. Each prints its duration withconsole.warnso whoever works on a feature, human or agent, can run the file and compare. The print is the point of those tests, so keep it, and never turn it into a duration assert. Theconsole.warnsummaries inrender-count-regression.test.tsx, median commit time included, are the same kind of feedback and the one exception to the logging rule above. That suite's contract stays in its render-count asserts.
Assertion style: no indirection, full values
- Assert complete literal values with
toEqual: the full value array, the full event array, the full operation objects. Deterministic keys make this possible. A full-value assertion pins ordering, count, and content at once, and drift shows up as a readable diff. - Do not build summarizer helpers (string transcripts, custom matchers, mapping functions) between the collected data and the assertion. The reader should see exactly what the editor emitted.
expect.objectContaining,expect.arrayContaining,expect.any,expect.anything,toMatchObject, andtoBeDefinedare banned. Thepte/no-weak-value-assertionslint rule fails on them in every test file.- A field that looks variable is almost always a key. Build the editor with
createTestKeyGeneratorand assert the generated keys literally. Create the key generator and fixtures inside each test, so the keys a test sees never depend on which tests ran before it. expect.stringContainingandexpect.stringMatchingstay allowed for error and warning text, where the full message would couple the test to its wording.- For value-shape assertions where the full tree is noise, use
toTextspec(editor.getSnapshot().context)and assert the textspec string ('B: foo bar|'), which is itself a full-value assertion in compact notation. - Exact-sequence event tests (
EventListenerPlugincollectingEditorEmittedEvents) assert the whole sequence; do not filter event types out to make assertions easier. - Prefer the full emitted payload sequence over counting callback calls. Keep a call count only where the count is the contract, such as how many times a registration runs.
Structure
- A scenario sets up exactly what its name says. A test named for an
[]initial value that passes noinitialValueis a copy of itsundefinedsibling. test('Scenario: ...')naming for integration tests. Every integration test carries its ownScenario:; adescribemay group by mechanism but never carries the scenario, narration, or a comment preamble.- Unit suites covering a single function group under
describe(fn.name, ...)(for exampledescribe(buildIndexMaps.name, ...)), tying the group to the symbol it pins and surviving renames. Every otherdescribeand every test name is prose describing behavior. A quoted identifier string is the one banned form:describe('isActiveListItem', ...)goes stale on rename without gaining readability. - Test files hold comments to the same bar as source files: the default is none. The scenario name and the full-value assertions are the documentation; narrative about why a contract exists belongs in the commit body, not in a comment above the test.
- A known-broken contract is pinned with
test.failsasserting the desired behavior: it documents the bug executably, and the fix flips it to a plaintest(CI reports it as unexpectedly passing until someone does). This holds when a new suite pins existing behavior too: where today's behavior is wrong, assert the right behavior undertest.failsinstead of pinning the bug as if it were the contract. - No bare
test.skip. Make the race deterministic, or pin a reproducible bug withtest.fails. - Reach code through the public or observable path. Do not test a private function only to cover an arm no caller reaches: an arm no caller reaches is dead code, not a test gap.
- A quote character inside a test name is solved by switching the string's quotes (
"...set's fallout..."), never with\uescapes or backslash-escaping. Prettier keeps whichever quote style avoids the escape. - Tests first, helpers below (per repo AGENTS.md: helper functions below main functions).
- Fixtures are plain functions returning complete objects (
function block(key, text): PortableTextBlock), local to the test file. - Comments explain why, placed inside the scope they explain, not above it.
- When test code duplicates production logic from elsewhere (a copied helper in a plugin), port the source's test suite wholesale as the drift alarm, with a header comment naming the source and the keep-in-sync contract.
Changing existing tests
- Coverage never shrinks. Every test that ran before still runs, and every assertion is at least as strong. When a case is removed as a duplicate, name the test that still covers it.
- A test that claims to pin a mechanism must be shown red with that mechanism broken (a local edit, never committed). If it stays green, it does not pin the mechanism: report the gap instead of counting it as coverage.
- When replacing local fixtures with a shared helper, the built values must stay deep-equal. If the helper cannot produce the same value, keep the local fixture. Never change an assertion to fit a helper.
Gates before pushing
From the package: pnpm check:types, pnpm test:unit, pnpm test:browser (or :chromium while iterating), pnpm build. From the repo root: pnpm check:lint, pnpm check:knip. The unit project fails on "no tests found": a package with only browser tests needs at least one .test.ts, which the drift-alarm suite usually provides.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

