plans/025-resize-unit-tests.md
Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the "STOP conditions" section occurs, stop and report — do not improvise. When done, update the status row for this plan in
plans/README.md— unless a reviewer dispatched you and told you they maintain the index.Drift check (run first):
git diff --stat 42bfbe3ed..HEAD -- packages/motion-dom/src/resizeIf any in-scope file changed since this plan was written, compare the "Current state" excerpts against the live code before proceeding; on a mismatch, treat it as a STOP condition.
42bfbe3ed, 2026-06-11resize() is public API on the motion package (exported via packages/motion-dom/src/index.ts:99 → framer-motion/dom → motion) and has zero tests. The module contains exactly the kind of shared-singleton bookkeeping that regresses silently: one module-level ResizeObserver shared across all subscriptions, a WeakMap of handler Sets deciding when to unobserve, and a module-level window-resize listener that must tear down when the last subscriber leaves and re-establish on re-subscription. A mock helper already exists at packages/motion-dom/src/resize/__tests__/mock-resize-observer.ts but nothing imports it — it's dead code that suggests tests were intended and never written. This plan writes the characterization suite so future refactors (and the open drag-QoL plan 021, which touches resize throttling) have a regression gate.
packages/motion-dom/src/resize/index.ts — public resize(): dispatches on argument type to resizeWindow(fn) or resizeElement(target, fn).packages/motion-dom/src/resize/handle-element.ts — the element path. Key facts to characterize:// handle-element.ts:5-7
const resizeHandlers = new WeakMap<Element, Set<ResizeHandler<Element>>>()
let observer: ResizeObserver | undefined
// handle-element.ts:30-41 — entries report border-box size when available,
// falling back to getBBox() for SVG, else offsetWidth/offsetHeight
function notifyTarget({ target, borderBoxSize }: ResizeObserverEntry) {
resizeHandlers.get(target)?.forEach((handler) => {
handler(target, {
get width() { return getWidth(target, borderBoxSize) },
get height() { return getHeight(target, borderBoxSize) },
})
})
}
// handle-element.ts:73-83 — cleanup unobserves only when the last handler leaves
return () => {
elements.forEach((element) => {
const elementHandlers = resizeHandlers.get(element)
elementHandlers?.delete(handler)
if (!elementHandlers?.size) {
observer?.unobserve(element)
}
})
}
Also note createResizeObserver() (lines 47–51) no-ops when typeof ResizeObserver === "undefined", and all observer calls are optional-chained — SSR-safe by design.
packages/motion-dom/src/resize/handle-window.ts — the window path: module-level Set of callbacks, lazily-attached "resize" listener, removed and reset to undefined when the last callback unsubscribes (lines 29–39).packages/motion-dom/src/resize/__tests__/mock-resize-observer.ts — the dead mock: installs window.ResizeObserver, tracks observed elements in a Set, stores the constructor callback, and captures the instance in a module-level activeObserver. Read the whole file first — it may lack an exported trigger/getter; extending it (e.g. exporting a getActiveObserver() or notify(entries) helper) is in scope. Model any such accessor on packages/framer-motion/src/utils/__tests__/mock-intersection-observer.ts's getActiveObserver() pattern.packages/motion-dom/jest.config.json (jsdom environment). Existing exemplar test for structure/style: packages/motion-dom/src/gestures/utils/__tests__/is-primary-pointer.test.ts.ResizeObserver (hence the mock) and offsetWidth/offsetHeight are always 0; getBBox doesn't exist on jsdom SVG elements. Size assertions must come from mock-provided borderBoxSize entries ([{ inlineSize: N, blockSize: M }]) or from stubbing the element properties (Object.defineProperty(el, "offsetWidth", { value: 100 })).| Purpose | Command | Expected on success |
|---|---|---|
| Run resize tests | npx jest --config packages/motion-dom/jest.config.json --testPathPattern="resize" (repo root) | all pass |
| Full motion-dom unit tests | npx jest --config packages/motion-dom/jest.config.json | no new failures |
| Lint | yarn lint (repo root) | exit 0 |
In scope (the only files you should modify/create):
packages/motion-dom/src/resize/__tests__/resize.test.ts (create)packages/motion-dom/src/resize/__tests__/mock-resize-observer.ts (extend only — e.g. add an exported accessor/trigger; keep existing shape)Out of scope (do NOT touch):
packages/motion-dom/src/resize/ (index.ts, handle-element.ts, handle-window.ts, types.ts) — this plan is characterization only. If a test reveals a genuine bug, STOP and report; do not fix source in this plan.packages/motion-dom/src/render/dom/scroll/** — scroll does not use resize(); ignore any apparent similarity.test/resize-unit-testsgit log): test(resize): add unit tests for element and window resize handlingRead mock-resize-observer.ts fully. Ensure the test file can (a) install the mock before importing the module under test, and (b) trigger entries. Important module-state caveat: handle-element.ts captures observer in module scope on first use — jest.resetModules() + re-require in beforeEach is the reliable way to get a fresh observer/mock pairing per test (the window path's windowCallbacks/windowResizeHandler are also module-level). Structure the suite accordingly from the start.
Verify: a trivial first test (resize(el, handler) then mock-trigger → handler called) passes via the resize test pattern command.
In resize.test.ts, cover:
borderBoxSize: [{ inlineSize: 100, blockSize: 50 }] → handler receives info.width === 100, info.height === 50.borderBoxSize, having stubbed offsetWidth/offsetHeight via Object.defineProperty → handler receives the stubbed values.resize() calls, same element → one observe per call is fine, but both handlers fire per entry; removing one (call its cleanup) leaves the other firing and does not unobserve.resize() calls occupies one Set slot — the first cleanup silences both subscriptions. Name the test so it reads as documented behavior, e.g. "KNOWN BEHAVIOR: duplicate handler reference is deduped across subscriptions".resize(".box", handler) resolves elements via document.querySelectorAll (append two matching divs to document.body); both get observed.window (fresh module registry), call resize(el, handler) → no throw, cleanup function still callable.Verify: resize test pattern → all pass.
resize(handler), dispatch window.dispatchEvent(new Event("resize")) → handler called with info.width === window.innerWidth, info.height === window.innerHeight.windowResizeHandler = undefined reset).Verify: resize test pattern → all pass. Then the full motion-dom suite → no new failures (known pre-existing failures in this repo's suites — e.g. SSR TextEncoder — are ignorable if they appear; they are unrelated to resize).
Verify: yarn lint → exit 0.
This plan is the test plan — Steps 2–3 enumerate the cases. SVG getBBox fallback is deliberately left untested: jsdom SVG elements lack getBBox, and isSVGElement(target) && "getBBox" in target guards it; faking both adds mock complexity for a two-line code path. Note this omission in a comment at the top of the test file.
Machine-checkable. ALL must hold:
packages/motion-dom/src/resize/__tests__/resize.test.ts exists with ≥9 tests, all passing via the resize test pattern commandgrep -rn "mock-resize-observer" packages/motion-dom/src --include="*.ts" shows at least one import outside the mock file itselfgit status shows no modified source files under packages/motion-dom/src/resize/ other than the two in-scope test filesplans/README.md status row updatedStop and report back (do not improvise) if:
jest.resetModules() — report rather than writing order-dependent tests.handle-element.ts/handle-window.ts to make them more testable — that's out of scope; report what blocked you.activeObserver is module-level; keep any added accessor read-only (mirror mock-intersection-observer.ts).