FN-5913: fix dashboard spinner anchoring
Keep dashboard loading spinners rotating around the correct center. - switch shared SVG spinner utilities from fill-box to view-box anchoring - factor spinner CSS assertions into a shared contract helper - add lucide Loader2 coverage and a regression test against fill-box anchoring Files changed: .../app/__tests__/spinner-animation.css.test.ts | 50 ++++++++++++++-------- packages/dashboard/app/styles.css | 2 +- 2 files changed, 34 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-5913 Fusion-Task-Lineage: 5fbf2577-301e-4813-ac15-30a73267d155
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
function extractBlock(content: string, pattern: RegExp): string {
|
||||
const match = content.match(pattern);
|
||||
@@ -20,31 +23,44 @@ function extractBlock(content: string, pattern: RegExp): string {
|
||||
return content.slice(match!.index!, index);
|
||||
}
|
||||
|
||||
function assertSharedSpinnerCssContract(css: string): void {
|
||||
const topLevelSpinBlock = extractBlock(css, /@keyframes\s+spin\s*\{/);
|
||||
const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
const svgSpinnerBlock = css.match(/svg\.animate-spin,\s*svg\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
|
||||
expect(topLevelSpinBlock).toContain("transform: rotate(360deg);");
|
||||
expect(css.indexOf("@keyframes spin")).toBeLessThan(css.indexOf(":root {\n --bg:"));
|
||||
|
||||
expect(animateSpinBlock).toContain("animation: spin 1s linear infinite;");
|
||||
expect(spinBlock).toContain("animation: spin 1s linear infinite;");
|
||||
expect(animateSpinBlock).toContain("transform-origin: center;");
|
||||
expect(spinBlock).toContain("transform-origin: center;");
|
||||
|
||||
expect(svgSpinnerBlock).toContain("transform-box: view-box;");
|
||||
expect(svgSpinnerBlock).not.toContain("transform-box: fill-box;");
|
||||
}
|
||||
|
||||
describe("global spinner animation utility", () => {
|
||||
const css = readFileSync(resolve(__dirname, "../styles.css"), "utf8");
|
||||
|
||||
it("keeps top-level spin keyframes rotating to 360deg", () => {
|
||||
const topLevelSpinBlock = extractBlock(css, /@keyframes\s+spin\s*\{/);
|
||||
|
||||
expect(topLevelSpinBlock).toContain("transform: rotate(360deg);");
|
||||
expect(css.indexOf("@keyframes spin")).toBeLessThan(css.indexOf(":root {\n --bg:"));
|
||||
it("keeps the shared spin utility centered and rotating infinitely", () => {
|
||||
assertSharedSpinnerCssContract(css);
|
||||
});
|
||||
|
||||
it("keeps the shared animate-spin and spin utilities running infinitely", () => {
|
||||
const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
it("keeps the svg spinner contract aligned with lucide stroke-only loaders", () => {
|
||||
render(React.createElement(Loader2, { className: "animate-spin", "data-testid": "spinner" }));
|
||||
|
||||
expect(animateSpinBlock).toContain("animation: spin 1s linear infinite;");
|
||||
expect(spinBlock).toContain("animation: spin 1s linear infinite;");
|
||||
const spinner = screen.getByTestId("spinner");
|
||||
expect(spinner.tagName.toLowerCase()).toBe("svg");
|
||||
expect(spinner).toHaveAttribute("class", expect.stringContaining("animate-spin"));
|
||||
expect(spinner).toHaveAttribute("fill", "none");
|
||||
expect(spinner).toHaveAttribute("viewBox", "0 0 24 24");
|
||||
});
|
||||
|
||||
it("anchors SVG spinners around their own center", () => {
|
||||
const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
const svgSpinnerBlock = css.match(/svg\.animate-spin,\s*svg\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
|
||||
it("fails the contract if svg spinners regress back to fill-box anchoring", () => {
|
||||
const regressedCss = css.replace("transform-box: view-box;", "transform-box: fill-box;");
|
||||
|
||||
expect(animateSpinBlock).toContain("transform-origin: center;");
|
||||
expect(spinBlock).toContain("transform-origin: center;");
|
||||
expect(svgSpinnerBlock).toContain("transform-box: fill-box;");
|
||||
expect(() => assertSharedSpinnerCssContract(regressedCss)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,7 +197,7 @@ html {
|
||||
|
||||
svg.animate-spin,
|
||||
svg.spin {
|
||||
transform-box: fill-box;
|
||||
transform-box: view-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user