Files
MealQuest/web/src/test/matchMedia.ts
T
dev_agentandClaude Opus 5.5 b2a4e66675
CI / ci (push) Successful in 8m7s
CI / ci (pull_request) Successful in 8m8s
Reflow tablet portrait layouts: Plan week 4 + 3, Settings one column
At 768-1199 px held upright the shell marks itself portrait (new
useOrientation hook over the existing tablet breakpoint) and trims its side
padding; the Plan week lays out four days over three instead of scrolling
sideways, and Settings (mock 2e) stacks its two columns into one. Vitest
covers every primary route at 820x1180 and 1180x820.

Task: 529c743e-4464-479b-b750-7ec41a0c9772

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:10:16 -04:00

59 lines
2.3 KiB
TypeScript

import { act } from '@testing-library/react'
type Listener = (event: MediaQueryListEvent) => void
// Replaces window.matchMedia with one that answers min-width / max-width
// queries for a pretend viewport width, orientation: portrait for its height
// (a viewport given no height is landscape), and prefers-reduced-motion:
// reduce with reducedMotion (jsdom has no matchMedia and no layout). Returns
// a function that resizes the pretend viewport and notifies every query
// whose answer changed.
export function mockViewport(
initialWidth: number,
{ reducedMotion = false, height: initialHeight = 0 } = {},
): (width: number, height?: number) => void {
let width = initialWidth
let height = initialHeight
const lists: { list: MediaQueryList; listeners: Set<Listener> }[] = []
function evaluate(query: string): boolean {
if (query === '(prefers-reduced-motion: reduce)') return reducedMotion
if (query === '(orientation: portrait)') return height >= width
const min = /\(min-width:\s*(\d+)px\)/.exec(query)
const max = /\(max-width:\s*(\d+)px\)/.exec(query)
if (!min && !max) throw new Error(`mockViewport cannot evaluate ${JSON.stringify(query)}`)
return (!min || width >= Number(min[1])) && (!max || width <= Number(max[1]))
}
window.matchMedia = (query: string) => {
const listeners = new Set<Listener>()
const list = {
media: query,
get matches() {
return evaluate(query)
},
onchange: null,
addEventListener: (_type: string, listener: Listener) => listeners.add(listener),
removeEventListener: (_type: string, listener: Listener) => listeners.delete(listener),
addListener: (listener: Listener) => listeners.add(listener),
removeListener: (listener: Listener) => listeners.delete(listener),
dispatchEvent: () => true,
} as unknown as MediaQueryList
lists.push({ list, listeners })
return list
}
return (nextWidth: number, nextHeight = 0) => {
const before = lists.map(({ list }) => list.matches)
width = nextWidth
height = nextHeight
act(() => {
lists.forEach(({ list, listeners }, i) => {
if (list.matches === before[i]) return
const event = { matches: list.matches, media: list.media } as MediaQueryListEvent
listeners.forEach((listener) => listener(event))
})
})
}
}