test(insights): pin the reduced-motion guard for .animate-insight-in

The 400 ms `.animate-insight-in` entrance keyframe is applied across five
insight surfaces and is collapsed to `animation: none` once, centrally, in
a `@media (prefers-reduced-motion: reduce)` block in globals.css. Extend
the motion-reduce coverage test to assert that central guard so a future
edit cannot silently drop it and replay the entrance motion for
motion-sensitive users.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 15:36:20 +02:00
1 parent 60b86f1ec9
commit e63fae564b
1 file changed
+53
@@ -65,3 +65,56 @@ describe("motion-reduce coverage — every animate-spin pairs with motion-reduce
).toEqual([]);
});
});
/**
* v1.11.3 — `.animate-insight-in` reduced-motion guard.
*
* Unlike `animate-spin` (a Tailwind utility paired with an inline
* `motion-reduce:animate-none` per site), `.animate-insight-in` is a custom
* keyframe utility defined in `globals.css` and applied across five insight
* surfaces (recommendations-grid, recommendation-card, insight-status-card,
* hero-strip, arztbericht-hero-card). Guarding it at each call site would be
* fragile, so the guard lives once in `globals.css`: a
* `@media (prefers-reduced-motion: reduce)` block collapses the animation to
* `none`, covering every consumer at once. This test pins that central guard
* so a future edit cannot drop it and silently re-introduce a 400 ms motion
* for motion-sensitive users.
*/
describe("motion-reduce coverage — animate-insight-in collapses under prefers-reduced-motion", () => {
const GLOBALS = join(process.cwd(), "src", "app", "globals.css");
const css = readFileSync(GLOBALS, "utf8");
it("globals.css defines the .animate-insight-in utility", () => {
expect(css).toMatch(/\.animate-insight-in\s*\{[^}]*animation:/);
});
it("a prefers-reduced-motion block collapses .animate-insight-in to no motion", () => {
// Locate the reduced-motion media query, then assert that within its
// body `.animate-insight-in` is reset to `animation: none`. The check
// is anchored on the media query so a stray `.animate-insight-in {
// animation: none }` outside a reduced-motion context cannot satisfy it.
const mediaStart = css.search(
/@media\s*\(\s*prefers-reduced-motion:\s*reduce\s*\)/,
);
expect(
mediaStart,
"Expected a `@media (prefers-reduced-motion: reduce)` block in globals.css.",
).toBeGreaterThanOrEqual(0);
// Scan from the media query to the end of the file — every motion the app
// defers under reduced motion lives in such a block, and `.animate-insight-in`
// must be one of them.
const fromMedia = css.slice(mediaStart);
const guarded = /\.animate-insight-in\s*\{\s*animation:\s*none/.test(
fromMedia,
);
expect(
guarded,
"Expected `.animate-insight-in { animation: none }` inside a " +
"`@media (prefers-reduced-motion: reduce)` block in globals.css. " +
"Without it the 400 ms entrance keyframe plays for motion-sensitive " +
"users on every insight card.",
).toBe(true);
});
});