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

ts
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

ts
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

ts
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 sends pointermove into the same element checks nothing, and does it silently;
  • PointerEvent is not implemented in jsdom, and trigger('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

ts
import { composingKeydown, keydown } from '@feugene/granularity/testing'

keydown(input, 'Enter')
composingKeydown(input, 'Enter') // the same during IME composition

isComposing 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

ts
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

ts
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

WhereWhat is there
@feugene/granularity/testingWhatever reaches into the core’s internals: the config, the announcer, the portal root
@feugene/granularity-test-kitEverything 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.

Last reviewed: 2026-09-01