289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { __shouldKeepTemperatureChartLoadingForTest } from "@/components/dashboard/scan-terminal/TemperatureChartCanvas";
|
|
|
|
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 usageGuideSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "UsageGuideDashboard.tsx"),
|
|
"utf8",
|
|
);
|
|
const chartSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "LiveTemperatureThresholdChart.tsx"),
|
|
"utf8",
|
|
);
|
|
const terminalPageSource = fs.readFileSync(
|
|
path.join(projectRoot, "app", "terminal", "page.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",
|
|
);
|
|
const scanQuerySource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "use-scan-terminal-query.ts"),
|
|
"utf8",
|
|
);
|
|
const mobileRegionTabsSource = fs.readFileSync(
|
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "MobileRegionTabs.tsx"),
|
|
"utf8",
|
|
);
|
|
|
|
assert(
|
|
dashboardSource.includes("MAX_TERMINAL_CHARTS = 6"),
|
|
"terminal dashboard must cap the desktop homepage at 6 charts",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("MOBILE_TERMINAL_CHARTS = 1"),
|
|
"terminal dashboard must document that mobile renders exactly one chart",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("MAX_TERMINAL_GRID_ROWS = 2") &&
|
|
dashboardSource.includes("clampGridRows") &&
|
|
dashboardSource.includes("Math.min(MAX_TERMINAL_CHARTS"),
|
|
"terminal grid dimensions must clamp saved 3x3 layouts down to the 3x2 maximum 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(
|
|
dashboardSource.includes("mobile-region-tabs-shell") &&
|
|
dashboardSource.includes("handleMobileSelectTab") &&
|
|
dashboardSource.includes("mobileSelectedRowInActiveGroup") &&
|
|
dashboardSource.includes("mobileActiveGroup?.rows[0]") &&
|
|
mobileRegionTabsSource.includes("min-h-11") &&
|
|
mobileRegionTabsSource.includes("snap-x") &&
|
|
mobileRegionTabsSource.includes("aria-pressed={isActive}"),
|
|
"mobile region navigation must stay visible above the landscape hint, remain touch-friendly, and switch the visible chart with the active region",
|
|
);
|
|
assert(
|
|
!dashboardSource.includes("disableClose={visibleSlots.filter(Boolean).length <= 1}"),
|
|
"desktop chart slots must allow clearing the last visible city so users can close a 1x1 chart",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("readStoredTerminalSlots") &&
|
|
dashboardSource.includes("hasPersistedSlots") &&
|
|
dashboardSource.includes("shouldAutofillInitialSlots"),
|
|
"terminal slots must distinguish first-load default fill from a user-cleared all-empty layout",
|
|
);
|
|
assert(
|
|
selectorSource.includes("[1, 2, 3].map") &&
|
|
selectorSource.includes("[1, 2].map") &&
|
|
selectorSource.includes("grid grid-cols-3") &&
|
|
!selectorSource.includes("[1, 2, 3].map((r)"),
|
|
"grid selector must expose at most a 3 by 2 chart layout",
|
|
);
|
|
assert(
|
|
usageGuideSource.includes("1x1 到 3x2") &&
|
|
usageGuideSource.includes("1x1 to 3x2") &&
|
|
!usageGuideSource.includes("1x1 到 3x3") &&
|
|
!usageGuideSource.includes("1x1 to 3x3"),
|
|
"terminal guide must describe the new 3x2 maximum layout instead of 3x3",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("LoadingSlotCard") &&
|
|
dashboardSource.includes("cityInSlot && !rowForSlot && refreshing") &&
|
|
dashboardSource.includes("handleSelectCityForSlot(slotIndex, null);"),
|
|
"terminal must show a loading chart card for saved cities while the first scan rows are still loading, then fall back to the empty picker for stale slots",
|
|
);
|
|
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(
|
|
terminalPageSource.includes("dynamic(") &&
|
|
terminalPageSource.includes('import("@/components/dashboard/ScanTerminalDashboard")') &&
|
|
terminalPageSource.includes("DashboardShellSkeleton") &&
|
|
!terminalPageSource.includes('import { ScanTerminalDashboard } from "@/components/dashboard/ScanTerminalDashboard";'),
|
|
"terminal route must dynamically load the heavy dashboard behind the shell skeleton",
|
|
);
|
|
assert(
|
|
dashboardSource.includes('from "next/dynamic"') &&
|
|
dashboardSource.includes('import("@/components/dashboard/scan-terminal/TrainingDashboard")') &&
|
|
!dashboardSource.includes('import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard";'),
|
|
"terminal dashboard must lazy-load the training analytics tab so Recharts stays out of the default terminal path",
|
|
);
|
|
assert(
|
|
chartSource.includes('import { TemperatureChartCanvas } from "@/components/dashboard/scan-terminal/TemperatureChartCanvas";') &&
|
|
!chartSource.includes("TemperatureChartCanvasFallback") &&
|
|
!chartSource.includes("import(\"@/components/dashboard/scan-terminal/TemperatureChartCanvas\")"),
|
|
"terminal temperature charts must load the Recharts canvas with the terminal route so visible cards do not sit behind a second-stage fallback",
|
|
);
|
|
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 dense 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",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("useDeferredValue") &&
|
|
dashboardSource.includes("deferredSearchQuery") &&
|
|
dashboardSource.includes("[rows, deferredSearchQuery]"),
|
|
"terminal search must defer expensive row filtering so typing stays responsive",
|
|
);
|
|
assert(
|
|
scanQuerySource.includes("MAX_STALE_SCAN_CACHE_MS") &&
|
|
scanQuerySource.includes("allowStale") &&
|
|
scanQuerySource.includes("setCachedRows(readScanCache(tradingRegion || \"\", { allowStale: true }))"),
|
|
"terminal data hook must render stale scan rows immediately while revalidating the first-screen API",
|
|
);
|
|
assert(
|
|
citySelectorSource.includes("useDeferredValue") &&
|
|
citySelectorSource.includes("deferredSearchQuery") &&
|
|
citySelectorSource.includes("[rows, deferredSearchQuery, activeTab]"),
|
|
"city selector search must defer expensive dropdown filtering so top-row selection stays responsive",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("accessDecisionPending") &&
|
|
dashboardSource.includes("shouldShowPaywall") &&
|
|
dashboardSource.indexOf("if (accessDecisionPending)") <
|
|
dashboardSource.indexOf("if (shouldShowPaywall)"),
|
|
"terminal must keep showing verification while access is undecided instead of flashing the paywall",
|
|
);
|
|
assert(
|
|
dashboardSource.includes('trackAppEvent("enter_terminal"') &&
|
|
dashboardSource.includes('entry: "terminal"'),
|
|
"terminal must emit enter_terminal when an entitled user reaches the dashboard",
|
|
);
|
|
assert(
|
|
chartCanvasSource.includes("memo(") &&
|
|
chartCanvasSource.includes("TemperatureChartCanvasComponent"),
|
|
"temperature chart canvas must be memoized so unrelated terminal state does not remount Recharts",
|
|
);
|
|
assert(
|
|
dashboardSource.includes("const TerminalSidebar = memo(function TerminalSidebar") &&
|
|
dashboardSource.includes("const [navExpanded, setNavExpanded] = useState(false);") &&
|
|
dashboardSource.includes("transition-colors duration-150") &&
|
|
!dashboardSource.includes("transition-all duration-200"),
|
|
"terminal sidebar expand state must be isolated from the chart grid and must not animate width through transition-all",
|
|
);
|
|
assert(
|
|
__shouldKeepTemperatureChartLoadingForTest({
|
|
row: { city: "Moscow" } as any,
|
|
isHourlyLoading: true,
|
|
activeSeries: [],
|
|
probabilityOverlay: null,
|
|
zoomedData: [
|
|
{ label: "00:00", ts: 1 },
|
|
{ label: "05:00", ts: 2 },
|
|
],
|
|
}),
|
|
"temperature chart should show the loading skeleton while the first detail fetch is in flight and no drawable data exists",
|
|
);
|
|
assert(
|
|
chartCanvasSource.includes("animate-pulse") &&
|
|
chartCanvasSource.includes("Loading chart") &&
|
|
chartCanvasSource.includes("bg-gradient-to-r"),
|
|
"temperature chart loading skeleton must include visible animation on cold first-open charts",
|
|
);
|
|
assert(
|
|
!__shouldKeepTemperatureChartLoadingForTest({
|
|
row: { city: "Moscow" } as any,
|
|
isHourlyLoading: false,
|
|
activeSeries: [],
|
|
probabilityOverlay: null,
|
|
zoomedData: [
|
|
{ label: "00:00", ts: 1 },
|
|
{ label: "05:00", ts: 2 },
|
|
],
|
|
}),
|
|
"temperature chart must stop showing an indefinite loading overlay after the detail fetch finishes without drawable data",
|
|
);
|
|
assert(
|
|
!__shouldKeepTemperatureChartLoadingForTest({
|
|
row: { city: "Moscow" } as any,
|
|
isHourlyLoading: false,
|
|
activeSeries: [
|
|
{
|
|
key: "current",
|
|
label: "Current reference",
|
|
source: "Live",
|
|
color: "#009688",
|
|
values: [13, 13],
|
|
},
|
|
] as any,
|
|
probabilityOverlay: null,
|
|
zoomedData: [
|
|
{ label: "00:00", ts: 1, current: 13 },
|
|
{ label: "05:00", ts: 2, current: 13 },
|
|
],
|
|
}),
|
|
"temperature chart should render once a visible series has drawable values",
|
|
);
|
|
assert(
|
|
__shouldKeepTemperatureChartLoadingForTest({
|
|
row: { city: "Moscow" } as any,
|
|
isHourlyLoading: true,
|
|
activeSeries: [
|
|
{
|
|
key: "current",
|
|
label: "Current reference",
|
|
source: "Live",
|
|
color: "#009688",
|
|
values: [13, 13],
|
|
},
|
|
] as any,
|
|
probabilityOverlay: null,
|
|
zoomedData: [
|
|
{ label: "00:00", ts: 1, current: 13 },
|
|
{ label: "05:00", ts: 2, current: 13 },
|
|
],
|
|
}),
|
|
"temperature chart should keep the loading overlay when the only drawable series is the fallback Current reference line",
|
|
);
|
|
}
|