Core
Testing
@weft-js/core/test renders components under vitest and drives them as a user would. weft check also flags tests that would pass whatever the code did.
A new app has no test runner. Add one:
npm install -D vitest jsdom @testing-library/dom @testing-library/user-event<!-- example: skip @weft-js/core/vite has no type declarations yet (TS7016 under strict) -->
// vitest.config.tsimport weft from '@weft-js/core/vite'import { defineConfig } from 'vitest/config'export default defineConfig({ plugins: [weft()], test: { environment: 'jsdom' } })Then npx vitest run. A test:
import { state } from '@weft-js/core'import { render, screen, user } from '@weft-js/core/test'import { expect, test } from 'vitest'function Cart() { const count = state(0) return <button type="button" onClick={() => count.value++}>Cart ({count.value})</button>}test('adding to the cart', async () => { render(Cart) await user.click(screen.getByRole('button', { name: 'Cart (0)' })) expect(screen.getByRole('button', { name: 'Cart (1)' })).toBeTruthy() await expect(() => user.click(screen.getByRole('button'))).toTouchAtMost(1, { runs: 2 })})render(Component, props, options)mounts into the document with a fresh app; unmounted after each test.options.stateseedsdefineStatescopes,options.storage(memoryStorage()) is shared between renders to play two tabs or a reload, andoptions.slotsbinds plugin slots.scope(handle)runs one scope with no UI.flush()applies pending updates. Everyuseraction andclock.advancealready ends in one.screenhas role, label and text queries only (getByRole,getByLabelText,getByText, withgetAll/query/find).user.click,dblClick,type,clear,keyboard,hover,unhover,tab,selectOptions,deselectOptions,uploadandpaste(@testing-library/user-event) each end inflush(): never sleep orwaitFor.clock.install()before the code schedules timers, thenawait clock.advance(ms)(orclock.runAll()): drives timers, rAF,Dateand the resources that wait on them, and flushes. Uninstalled after each test.touches(fn)returns{ mutations, runs, … }for one interaction.toTouchAtMost(n, { runs })bounds both: DOM writes alone miss identity bugs that re-run every row but write the DOM once (W014).
Junk tests: T001–T009
weft check reports these on test files (test.include in the config). A test that finishes without an assertion also fails at runtime (T001). T012 and T013 are about open listeners and real sockets: weft docs browser and weft docs start; T010, T011 and T020 are the gate: weft docs verify.
| Code | Catches | Instead |
|---|---|---|
| T001 | No assertion, or expect(x) with no matcher | Assert what the user sees |
| T002 | expect(true).toBe(true), expect(x).toBe(x) | Assert on the code's output |
| T003 | n.value = 5; expect(n.value).toBe(5) | Act through the UI, assert what depends on it |
| T004 | setTimeout sleeps, real timers, waitFor | flush(), clock.advance(ms) |
| T005 | container, selectors, classes, innerHTML, test ids, app state in a component test (warning) | Role, label, text |
| T006 | vi.mock('@weft-js/core'), mocking the component under test | Fake the edges (API, storage) |
| T007 | Snapshots | Behavior assertions |
| T008 | expect under if/?:/&&/catch, or only inside loops | Assert the condition, then assert unconditionally |
| T009 | .only, .skip, fit, xit | Remove it; test.fails for a test meant to fail |
A deliberate exception needs a reason: // weft-allow T007: the exported SVG is the product. In the file's header comments it covers the whole file; elsewhere, its own line and the next.
Plugins can use this clock too: fakePay from @weft-js/pay/test plays a rail on it, through your real webhook and ledger. weft docs pay.