106 lines
4.6 KiB
TypeScript
106 lines
4.6 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
function assert(condition: unknown, message: string) {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
export function runTests() {
|
|
const projectRoot = process.cwd();
|
|
const dashboardSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "ScanTerminalDashboard.tsx"),
|
|
"utf8",
|
|
);
|
|
const selectorSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "GridLayoutSelector.tsx"),
|
|
"utf8",
|
|
);
|
|
const chartSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "LiveTemperatureThresholdChart.tsx"),
|
|
"utf8",
|
|
);
|
|
const chartCanvasSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "TemperatureChartCanvas.tsx"),
|
|
"utf8",
|
|
);
|
|
const citySelectorSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "CitySelectorDropdown.tsx"),
|
|
"utf8",
|
|
);
|
|
|
|
assert(
|
|
dashboardSource.includes("MAX_TERMINAL_CHARTS = 9"),
|
|
"terminal dashboard must cap the desktop homepage at 9 charts",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("MOBILE_TERMINAL_CHARTS = 1"),
|
|
"terminal dashboard must document that mobile renders exactly one chart",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("clampGridSide") &&
|
|
dashboardSource.includes("Math.min(MAX_TERMINAL_CHARTS"),
|
|
"terminal grid dimensions must be clamped before rendering or persisting",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("mobileChartRow") &&
|
|
dashboardSource.includes("建议横屏") &&
|
|
dashboardSource.includes("Rotate to landscape") &&
|
|
dashboardSource.includes("disableClose={true}"),
|
|
"mobile terminal should render one selected chart and suggest landscape for the full grid",
|
|
);
|
|
assert(
|
|
selectorSource.includes("[1, 2, 3].map") &&
|
|
selectorSource.includes("grid grid-cols-3"),
|
|
"grid selector must expose at most a 3 by 3 chart layout",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("if (!cityInSlot || !rowForSlot)") &&
|
|
dashboardSource.includes("handleSelectCityForSlot(slotIndex, null);"),
|
|
"stale saved chart slots must render the empty city picker instead of a row=null Temperature Chart",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("absolute left-1/2 top-12 z-50") &&
|
|
!dashboardSource.includes("top-1/2 -translate-x-1/2 -translate-y-1/2"),
|
|
"empty top-row slots must open the city selector downward so the search input is not clipped by the viewport header",
|
|
);
|
|
assert(
|
|
citySelectorSource.includes("max-h-[calc(100vh-72px)]") &&
|
|
citySelectorSource.includes("min-h-0 flex-1 overflow-y-auto"),
|
|
"city selector dropdown must cap its viewport height and keep the result list scrollable",
|
|
);
|
|
assert(
|
|
citySelectorSource.includes("MIN_DROPDOWN_TOP_PX") &&
|
|
citySelectorSource.includes("viewportNudgeY") &&
|
|
citySelectorSource.includes("getBoundingClientRect()"),
|
|
"city selector dropdown must nudge itself inside the viewport when opened from top-row chart cards",
|
|
);
|
|
assert(
|
|
chartSource.includes("setLiveTemp(null);") &&
|
|
chartSource.includes("lastAppliedPatchRevisionRef.current = 0;"),
|
|
"switching city slots must clear the previous live temperature so Fahrenheit values cannot leak into Celsius charts",
|
|
);
|
|
assert(
|
|
chartSource.includes('compact ? "min-h-0" : "min-h-[300px]"') &&
|
|
chartCanvasSource.includes("const minChartHeight = compact ? 120 : 220") &&
|
|
chartCanvasSource.includes('compact ? "min-h-[120px]" : "min-h-[220px]"'),
|
|
"compact grid charts must not force desktop minimum heights that get clipped inside 3x3 terminal slots",
|
|
);
|
|
const signedOutBlock = dashboardSource.slice(
|
|
dashboardSource.indexOf('if (event === "SIGNED_OUT")'),
|
|
dashboardSource.indexOf('} else if (event === "TOKEN_REFRESHED"'),
|
|
);
|
|
assert(
|
|
signedOutBlock.includes("await supabase.auth.getSession()") &&
|
|
signedOutBlock.includes("mergeAccessStateWithAuthPayload(prev, payload)") &&
|
|
signedOutBlock.indexOf("await supabase.auth.getSession()") <
|
|
signedOutBlock.indexOf("setProAccess(createEmptyAccess(false))"),
|
|
"terminal auth listener must re-check the current Supabase session before clearing access on SIGNED_OUT events",
|
|
);
|
|
assert(
|
|
dashboardSource.includes('event === "INITIAL_SESSION"') &&
|
|
dashboardSource.indexOf('event === "INITIAL_SESSION"') <
|
|
dashboardSource.indexOf('event === "TOKEN_REFRESHED"'),
|
|
"terminal auth listener must hydrate access from Supabase INITIAL_SESSION events during first navigation from the landing page",
|
|
);
|
|
}
|