@mercury-fx/testing
v3.0.0
Published
Tests on Mercury Hyperscript: render with container-scoped queries, exact native events, act that drains the renderer and awaits what it is given, renderHook and a loop guard, on DOM Testing Library.
Readme
@mercury-fx/testing
Tests on Mercury Hyperscript, the runtime your users ship (mercury.testing.1). It uses one renderer (hyperscript's own render and act), DOM Testing Library's queries, and no React, React Testing Library or bare-Preact adapter.
import { act, cleanup, fireEvent, render, screen, typeInto, waitFor } from "@mercury-fx/testing";
afterEach(cleanup);
const view = render(<Search />); // queries are bound to this container
typeInto(view.getByRole("searchbox"), "alpha"); // input, then change
await waitFor(() => expect(view.getByText("Alpha")).toBeTruthy());| Export | What it does |
|---|---|
| render(ui, { container?, baseElement? }) | Mounts inside act and returns the queries bound to the container (or to baseElement, for a portal's host), plus rerender, unmount (idempotent) and debug. screen is document-wide and explicit. |
| cleanup() | Unmounts every tree render mounted, then removes only the containers render created. DOM the test owns itself stays. |
| act(callback) | Hyperscript's act: it runs the callback, awaits the promise the callback returns, and drains queued renders and effects. It spends no extra microtask turns. Work that wasn't returned (a promise chain an effect started, a timer) isn't done when act resolves; wait for its outcome with waitFor. Every failure is the returned promise's rejection. |
| fireEvent | DOM Testing Library's, each call inside act. It dispatches exactly the native event named: change is the DOM's commit event, and focus doesn't bubble (a container uses focusIn). Nothing is remapped. |
| typeInto(element, value) | Sets the value, then fires input and then change, as a user's typing and commit do. |
| renderHook(hook, { initialProps?, wrapper? }) | Gives result.current, rerender(props?) and unmount. rerender() with no argument re-renders with the last props given (initialProps until a rerender(props)); rerender(undefined) passes undefined. |
| guardRenders({ budget, label }) | Throws RenderBudgetExceeded from inside the render once a guarded stretch exceeds budget diffs, which fails an update loop while it runs. It chains options.diffed, and release() restores it. It's test-only. |
| screen, within, waitFor, waitForElementToBeRemoved, prettyDOM | Re-exported from @testing-library/dom. |
Peers: @mercury-fx/hyperscript (the same Mercury version) and @testing-library/dom 10. Provide a DOM environment such as Vitest's jsdom.
Commands
Run these from mercury/packages/mhs-testing.
| Command | Covers |
|---|---|
| pnpm run test:contract | MHT1 to MHT3: vitest run --config vitest.config.ts, on src/*.spec.ts(x) only, which the root config's *.test.* globs don't collect |
| pnpm run test:loop-watchdog | MHT3's negative control: the unstable fixture with the guard disabled (MHT3_NO_GUARD=1), in a child process ended by an outside 30 s watchdog. That's a failed run, never a pass. --guarded runs the same with the guard on, expecting it to end by itself |
| pnpm run check:closure | MHT6 and MHT7: the packed testing and hyperscript tarballs installed in a fresh consumer outside the workspace. It checks the dist imports, the install closure, a runtime render, click and guard in Node with jsdom, the packed manifest, and the adopters (no built dist imports this package). Needs pnpm run build here and in packages/hyperscript |
| pnpm run test:browser | MHT5 (T2): native interaction in a real headless Chromium through Playwright's driver (userEvent from vitest/browser), on src/browser/*.spec.tsx with vitest.browser.config.ts. It covers typing character by character before any blur, a checkbox's and a select's committed change, focus-within through focusin, double-click, and a portal dialog's keyboard close with focus back on its opener. Chromium comes from PLAYWRIGHT_BROWSERS_PATH; this lane is Chromium only |
| pnpm run typecheck | tsc --noEmit |
Negative controls, each of which must make its row fail:
node scripts/check-closure.mjs --mutant foreign-renderer: the packed bundle imports npm preact. Fails MHT7's manifest row and MHT6's install row.node scripts/check-closure.mjs --mutant drop-export:dist/testing.jsis missing from the tarball. Fails MHT6's runtime row.node scripts/loop-watchdog.mjs: the guard disabled. Ended by the watchdog.- In src/browser, by hand:
- the text input's
onInputchanged toonChangefails "typing updates visible state … before any blur"; - the container's
onFocusInchanged toonFocusfails the focus-within row.
- the text input's
- In src, by hand:
- queries bound to
document.bodyfail MHT1's two-root, portal and cleanup cases; - an
actwith ten extra microtask turns fails "act spends no extra turns"; - an
actthat swallows errors fails the two rejection cases.
- queries bound to
Owned scope and disposal (MHT4) is in the Effector adopter, packages/mercury-effector/src/testing-adoption.test.tsx. It measures the engine's own seam: a scoped watch is a node in the scope's additionalLinks.
