Files
PolyWeather/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
T

122 lines
4.6 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import {
MODEL_SUMMARY_MODEL_COLUMNS,
buildModelSummaryRows,
filterModelSummaryRows,
formatModelSummaryTemp,
} from "@/lib/model-summary";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
}
export function runTests() {
const rows = [
{
city: "paris",
city_display_name: "Paris",
trading_region_label: "Europe / Africa",
trading_region_label_zh: "欧洲 / 非洲",
trading_region_sort: 5,
temp_symbol: "°C",
current_max_so_far: 32,
deb_prediction: 31.6,
local_time: "2026-06-29T10:00",
model_cluster_sources: {
ECMWF: 32.2,
"ECMWF AIFS": 31.9,
GFS: 33.4,
ICON: 31.2,
"ICON-EU": 31.4,
GEM: 32.8,
GDPS: 32.5,
JMA: 30.9,
"AROME HD": 32.1,
},
},
{
city: "madrid",
city_display_name: "Madrid",
trading_region_label: "Europe / Africa",
trading_region_label_zh: "欧洲 / 非洲",
trading_region_sort: 5,
temp_symbol: "°C",
current_max_so_far: 34.2,
deb_prediction: null,
local_time: "2026-06-29T10:05",
model_cluster_sources: {
ECMWF: 37,
GFS: 38.2,
},
},
] as any;
const originalFirstModelSources = rows[0].model_cluster_sources;
const summaryRows = buildModelSummaryRows(rows, false);
assert(
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("AROME HD") &&
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("HRRR") &&
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("NAM"),
"model summary must expose the fixed model columns including optional short-range models",
);
assert(summaryRows.length === 2, "model summary should keep one row per city");
assert(summaryRows[0].cityName === "Madrid", "model summary should sort by region then city name");
assert(summaryRows[1].cityName === "Paris", "model summary should sort by region then city name");
assert(summaryRows[1].debPrediction === 31.6, "model summary should preserve DEB prediction");
assert(summaryRows[1].models.GFS === 33.4, "model summary should preserve model high temperature");
assert(summaryRows[1].models.HRRR === null, "missing models should be normalized to null");
assert(summaryRows[1].modelMedian === 32.1, "model median should use available model values only");
assert(summaryRows[1].modelSpread === 2.5, "model spread should use available model min/max only");
assert(formatModelSummaryTemp(null, "°C") === "—", "missing model temperatures should render as an em dash");
assert(formatModelSummaryTemp(32.16, "°C") === "32.2°C", "model temperatures should render to one decimal");
const searched = filterModelSummaryRows(summaryRows, {
debOnly: true,
query: "par",
wideSpreadOnly: false,
});
assert(searched.length === 1 && searched[0].cityName === "Paris", "model summary search and DEB filter should compose");
const wideSpread = filterModelSummaryRows(summaryRows, {
debOnly: false,
query: "",
wideSpreadOnly: true,
});
assert(
wideSpread.length === 1 && wideSpread[0].cityName === "Paris",
"wide-spread filter should only keep rows with model spread >= 2°C",
);
assert(rows[0].model_cluster_sources === originalFirstModelSources, "model summary filters must not mutate source rows");
const projectRoot = process.cwd();
const dashboardSource = fs.readFileSync(
path.join(projectRoot, "components", "dashboard", "ScanTerminalDashboard.tsx"),
"utf8",
);
const modelSummarySource = fs.readFileSync(
path.join(projectRoot, "components", "dashboard", "scan-terminal", "ModelSummaryDashboard.tsx"),
"utf8",
);
assert(
dashboardSource.includes("modelSummary") &&
dashboardSource.includes("模型汇总") &&
dashboardSource.includes("Model Summary") &&
dashboardSource.includes("Table2"),
"terminal sidebar must expose the model summary nav item",
);
assert(
dashboardSource.includes("<ModelSummaryDashboard") &&
dashboardSource.includes("rows={rows}") &&
dashboardSource.includes("generatedText={generatedText}"),
"terminal model summary view must use existing scan rows instead of fetching city detail",
);
assert(
modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") &&
modelSummarySource.includes("Only DEB") &&
modelSummarySource.includes("仅 DEB") &&
modelSummarySource.includes("Large spread") &&
modelSummarySource.includes("分歧较大"),
"model summary dashboard must render the fixed model table and filters",
);
}