Testing
Helpers for testing your application, the limits of jsdom, and the rules without which a test goes green on broken code.
Machine-translated, not yet reviewed. Read the original
The runner and the way you mount are yours: the subpath
@feugene/granularity/testing depends on neither vitest nor
@vue/test-utils and hands out only data and actions on the DOM. There are
deliberately no wrappers over fake timers and mocks — that is your tool’s API,
and the package must not break when its major version changes.
The component environment
import { granularityGlobal } from '@feugene/granularity/testing'
mount(GrSelect, { props, global: granularityGlobal({ size: 'sm' }) })
mount(GrButton, {
global: granularityGlobal({
componentDefaults: { GrButton: { variant: 'outline' } },
i18n: { 'gr.select.loading': 'Loading…' },
}),
})The helper hands out exactly what GrConfigProvider puts into provide. It
cannot be used to test the provider itself — that is what mounting it is for.
Cleaning up between tests
import { resetGranularityDom } from '@feugene/granularity/testing'
afterEach(resetGranularityDom)This is not hygiene but a condition of the thing working. A single
document.body.innerHTML = '' is not enough: the portal root and the live region
host are cached by their modules, and after that kind of cleanup they point at
nodes outside the document. The next mount teleports the overlays there, and
the test does not find them — without a single error in the console.
Gestures
import { drag, move, press, release } from '@feugene/granularity/testing'
press(handle, { clientY: 10 })
move({ clientY: 60 }) // into window, not into the element
release()Two reasons not to write this by hand:
- movement and release are listened for on
window. That is how the gesture works in the slider, the splitter, row and column reordering, the toast swipe: the press comes from the markup and the gesture is then driven globally — otherwise it would break the moment the cursor left the element. A test that sendspointermoveinto the same element checks nothing, and does it silently; PointerEventis not implemented in jsdom, andtrigger('pointerdown', { clientY })throws while writing a coordinate into an event type it does not know.
cancelPointer() checks a branch of its own: an interruption rolls the state
back rather than completing the gesture.
The keyboard and IME
import { composingKeydown, keydown } from '@feugene/granularity/testing'
keydown(input, 'Enter')
composingKeydown(input, 'Enter') // the same during IME compositionisComposing does not survive trigger('keydown', …): the property is read-only
and the assignment is silently lost. A test for “Enter during composition commits
nothing” written through trigger checks an ordinary Enter — that is, it goes
green on broken code.
The flag is set together with keyCode: 229, because Safari before 16.4 did not
set the first one; the package reads composition from both signs, and checking
half of the predicate is pointless.
Geometry
import { mockRect, stackRects } from '@feugene/granularity/testing'
mockRect(track, { width: 200 })
stackRects(rows, { size: 20 })There is no layout in jsdom: getBoundingClientRect is always zero. Anything
that computes a pointer hit or measures its neighbours — the slider track, the
rows of a reorderable list, column widths and offsets — is not verified on a zero
rectangle.
The live region
import { announced } from '@feugene/granularity/testing'
expect(await announced()).toBe('13 August 2026')The await is mandatory: an announcement is written into the region in a deferred macrotask, and that is a condition of it working — a region whose text appeared in the same frame as the region itself is not announced at all by some screen readers.
Where the line between packages runs
| Where | What is there |
|---|---|
@feugene/granularity/testing | Whatever reaches into the core’s internals: the config, the announcer, the portal root |
@feugene/granularity-test-kit | Everything else that is test-related: ./vue, ./a11y (axe in jsdom), ./e2e (keyboard traversal in Playwright) |
The line runs along knowledge of the package rather than along the word “test”.
Move granularityGlobal and resetGranularityDom into the test kit and it would
depend on the core, while the core calls its factories: the dependency would
close into a cycle.
What jsdom cannot check
Neither focus movement on Tab, nor activation of a button on Enter, nor
ResizeObserver. trigger('click') checks the handler but not that the handler
can be reached from the keyboard at all. The keyboard contract is therefore
verified in a live browser — in the library itself, and it is worth doing the
same in your application.