feat: implement interactive temperature chart dashboard with runway trend visualization and multi-source data collection support
This commit is contained in:
@@ -6,12 +6,23 @@ import {
|
||||
} from "@/lib/refresh-policy";
|
||||
import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
||||
import { __shouldPollLiveChartForTest } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
|
||||
import {
|
||||
MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
|
||||
__resetHourlyDetailRequestQueueForTest,
|
||||
__runQueuedHourlyDetailRequestForTest,
|
||||
} from "@/components/dashboard/scan-terminal/temperature-chart-logic";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
async function flushMicrotasks() {
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTests() {
|
||||
assert(DASHBOARD_REFRESH_POLICY_MS.observation === 60_000, "observation layer should refresh every 60 seconds");
|
||||
assert(DASHBOARD_REFRESH_POLICY_MS.scanRows === 5 * 60_000, "region/city rows should refresh every 5 minutes");
|
||||
assert(DASHBOARD_REFRESH_POLICY_MS.marketOverview === 10 * 60_000, "market overview should refresh every 10 minutes");
|
||||
@@ -68,4 +79,55 @@ export function runTests() {
|
||||
chartSource.includes("setHourly(seedHourlyForecastFromRow(row))"),
|
||||
"terminal charts should render from row data immediately and dedupe concurrent city detail requests",
|
||||
);
|
||||
|
||||
__resetHourlyDetailRequestQueueForTest();
|
||||
let activeRequests = 0;
|
||||
let maxActiveRequests = 0;
|
||||
let startedRequests = 0;
|
||||
const releases: Array<(() => void) | undefined> = [];
|
||||
const requestCount = MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS + 3;
|
||||
|
||||
const requests = Array.from({ length: requestCount }, (_, index) =>
|
||||
__runQueuedHourlyDetailRequestForTest(
|
||||
() =>
|
||||
new Promise<number>((resolve) => {
|
||||
startedRequests += 1;
|
||||
activeRequests += 1;
|
||||
maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
|
||||
releases[index] = () => {
|
||||
activeRequests -= 1;
|
||||
resolve(index);
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await flushMicrotasks();
|
||||
assert(
|
||||
startedRequests === MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
|
||||
"city detail queue should start only the configured number of concurrent requests",
|
||||
);
|
||||
assert(
|
||||
maxActiveRequests === MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
|
||||
"city detail queue must cap simultaneous full-detail requests",
|
||||
);
|
||||
|
||||
releases[0]?.();
|
||||
await flushMicrotasks();
|
||||
assert(
|
||||
startedRequests === MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS + 1,
|
||||
"city detail queue should start the next pending request when one active request finishes",
|
||||
);
|
||||
|
||||
for (let index = 1; index < requestCount; index += 1) {
|
||||
await flushMicrotasks();
|
||||
assert(Boolean(releases[index]), `city detail queue should eventually start queued request #${index}`);
|
||||
releases[index]?.();
|
||||
}
|
||||
const results = await Promise.all(requests);
|
||||
assert(
|
||||
results.length === requestCount && results.every((value, index) => value === index),
|
||||
"city detail queue should resolve every queued request in order",
|
||||
);
|
||||
__resetHourlyDetailRequestQueueForTest();
|
||||
}
|
||||
|
||||
@@ -91,12 +91,16 @@ export function runTests() {
|
||||
const chartStatsPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "TemperatureStatsBars.tsx");
|
||||
const chartRunwayPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "TemperatureRunwayDetails.tsx");
|
||||
const chartTooltipPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "TemperatureTooltipContent.tsx");
|
||||
const chartSummaryPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "ModelCurvesSummary.tsx");
|
||||
assert(fs.existsSync(chartCanvasPath), "temperature chart Recharts canvas must live in TemperatureChartCanvas.tsx");
|
||||
assert(fs.existsSync(chartStatsPath), "temperature chart stat bars must live in TemperatureStatsBars.tsx");
|
||||
assert(fs.existsSync(chartRunwayPath), "temperature chart runway detail panel must live in TemperatureRunwayDetails.tsx");
|
||||
assert(fs.existsSync(chartTooltipPath), "temperature chart tooltip must live in TemperatureTooltipContent.tsx");
|
||||
assert(fs.existsSync(chartSummaryPath), "temperature chart model summary must live in ModelCurvesSummary.tsx");
|
||||
const chartCanvas = fs.readFileSync(chartCanvasPath, "utf8");
|
||||
const chartTooltip = fs.readFileSync(chartTooltipPath, "utf8");
|
||||
const chartRunway = fs.readFileSync(chartRunwayPath, "utf8");
|
||||
const chartSummary = fs.readFileSync(chartSummaryPath, "utf8");
|
||||
const chartLogicPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "temperature-chart-logic.ts");
|
||||
assert(fs.existsSync(chartLogicPath), "temperature chart pure data logic must live in temperature-chart-logic.ts");
|
||||
const chartLogic = fs.readFileSync(chartLogicPath, "utf8");
|
||||
@@ -112,6 +116,18 @@ export function runTests() {
|
||||
assert(chart.includes("TemperatureChartCanvas"), "temperature chart shell must compose the extracted chart canvas");
|
||||
assert(chart.includes("TemperatureStatsBars"), "temperature chart shell must compose the extracted stat bars");
|
||||
assert(chart.includes("TemperatureRunwayDetails"), "temperature chart shell must compose the extracted runway panel");
|
||||
assert(chart.includes("tempSymbol={row?.temp_symbol || \"°C\"}"), "temperature chart shell must pass the city temperature unit into stat bars");
|
||||
assert(chart.includes("<TemperatureRunwayDetails") && chart.includes("tempSymbol={row?.temp_symbol || \"°C\"}"), "temperature chart shell must pass the city unit into runway details");
|
||||
assert(chart.includes("<ModelCurvesSummary") && chart.includes("tempSymbol={row?.temp_symbol || \"°C\"}"), "temperature chart shell must pass the city unit into model summaries");
|
||||
const chartStats = fs.readFileSync(chartStatsPath, "utf8");
|
||||
assert(chartStats.includes("tempSymbol"), "temperature stat bars must accept the city temperature unit");
|
||||
assert(chartStats.includes("temp(displayRunwayTemp, tempSymbol)"), "temperature stat bars must render live observations with the city unit");
|
||||
assert(chartRunway.includes("tempSymbol") && !chartRunway.includes("}°C`"), "runway detail rows must render values with the city unit");
|
||||
assert(chartSummary.includes("tempSymbol") && chartSummary.includes("temp(stats.latest, tempSymbol)"), "model curve summaries must render values with the city unit");
|
||||
assert(chartCanvas.includes("const tempSymbol = row?.temp_symbol || \"°C\""), "temperature chart canvas must derive the city unit from the row");
|
||||
assert(chartCanvas.includes("tempSymbol={tempSymbol}"), "temperature chart canvas must pass the city unit into tooltips");
|
||||
assert(chartCanvas.includes("tickFormatter={(v) => `${Number(v).toFixed(0)}${tempSymbol}`}"), "temperature y-axis ticks must include °C/°F instead of a bare degree mark");
|
||||
assert(chartTooltip.includes("tempSymbol"), "temperature tooltip must accept the city temperature unit");
|
||||
assert(!chart.includes("from \"recharts\""), "LiveTemperatureThresholdChart.tsx must not import Recharts directly");
|
||||
assert(!chart.includes("function TemperatureTooltipContent"), "LiveTemperatureThresholdChart.tsx must not define tooltip content inline");
|
||||
assert(chartCanvas.includes("TemperatureTooltipContent"), "temperature chart canvas must use a custom tooltip content component");
|
||||
|
||||
Reference in New Issue
Block a user