Compare commits
No commits in common. "737918985bb8eb2178bd0f6a140973539ae93bfe" and "c2d890ff4e9bdf075d617ebae163a74d4647223e" have entirely different histories.
737918985b
...
c2d890ff4e
8 changed files with 7 additions and 251 deletions
|
|
@ -38,20 +38,6 @@ const CONFIDENCE_STYLES: Record<
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Нейтральный fallback (#2452, тот же паттерн что VelocityBlock/ParcelDrawer):
|
|
||||||
// значение вне закрытого union'а (schema drift / частичный деплой) даёт
|
|
||||||
// `undefined`, и следующее обращение `.fg`/`.bg` роняет рендер всего блока.
|
|
||||||
// Серый + «—» — намеренно НЕ семантический цвет: неизвестное качество данных
|
|
||||||
// не должно выглядеть ни хорошим, ни плохим.
|
|
||||||
// Фон взят из --border-soft, а не из нового токена: список токенов закрыт
|
|
||||||
// (ui-tokens.md — «НЕ выдумывать новые»), а нейтральной заливки для бейджа в
|
|
||||||
// нём нет; --border-soft — единственный существующий нейтральный светло-серый.
|
|
||||||
const NEUTRAL_CONFIDENCE_STYLE = {
|
|
||||||
fg: "var(--fg-tertiary, #73767E)",
|
|
||||||
bg: "var(--border-soft, #EEF0F3)",
|
|
||||||
label: "—",
|
|
||||||
};
|
|
||||||
|
|
||||||
const TIME_WINDOW_LABELS: Record<TimeWindow, string> = {
|
const TIME_WINDOW_LABELS: Record<TimeWindow, string> = {
|
||||||
last_month: "Последний месяц",
|
last_month: "Последний месяц",
|
||||||
last_quarter: "Последний квартал",
|
last_quarter: "Последний квартал",
|
||||||
|
|
@ -76,14 +62,8 @@ const ROOM_BUCKET_LABELS: Record<string, string> = {
|
||||||
|
|
||||||
// ── Sub-components ────────────────────────────────────────────────────────────
|
// ── Sub-components ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Экспорт ради теста (#2452): карточка читает `confidence` из API по ключу
|
function DataQualityCard({ dq }: { dq: BestLayoutsResponse["data_quality"] }) {
|
||||||
// закрытого union'а, и её нужно проверять на out-of-union напрямую.
|
const conf = CONFIDENCE_STYLES[dq.confidence];
|
||||||
export function DataQualityCard({
|
|
||||||
dq,
|
|
||||||
}: {
|
|
||||||
dq: BestLayoutsResponse["data_quality"];
|
|
||||||
}) {
|
|
||||||
const conf = CONFIDENCE_STYLES[dq.confidence] ?? NEUTRAL_CONFIDENCE_STYLE;
|
|
||||||
return (
|
return (
|
||||||
// Inline-стили (НЕ Tailwind): flex/gap/bg-* не применяются на этой странице —
|
// Inline-стили (НЕ Tailwind): flex/gap/bg-* не применяются на этой странице —
|
||||||
// спаны текли inline и слипались («НизкоеПокрытие», #1953). Inline гарантирует
|
// спаны текли inline и слипались («НизкоеПокрытие», #1953). Inline гарантирует
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
/**
|
|
||||||
* #2452 (follow-up #2445-B2/B3) — `CONFIDENCE_STYLES[dq.confidence]` не имел
|
|
||||||
* fallback'а: значение вне закрытого union'а (schema drift / частичный деплой)
|
|
||||||
* давало `undefined`, и следующее обращение `.fg`/`.bg` роняло рендер всей
|
|
||||||
* карточки качества данных.
|
|
||||||
*
|
|
||||||
* Тесты фиксируют нейтральный fallback (тот же паттерн, что VelocityBlock и
|
|
||||||
* ParcelDrawer): неизвестное качество рисуется серым и «—», а не падает и не
|
|
||||||
* выдаёт себя за хорошее или плохое.
|
|
||||||
*/
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { DataQualityCard } from "../BestLayoutsBlock";
|
|
||||||
import type { BestLayoutsResponse } from "@/types/best-layouts";
|
|
||||||
|
|
||||||
type DataQuality = BestLayoutsResponse["data_quality"];
|
|
||||||
|
|
||||||
function makeDq(overrides: Partial<DataQuality> = {}): DataQuality {
|
|
||||||
return {
|
|
||||||
objects_with_velocity_data: 18,
|
|
||||||
objects_total_in_radius: 25,
|
|
||||||
raw_objects_total: 31,
|
|
||||||
groups_matched_by_core: 12,
|
|
||||||
groups_with_supply_only: 3,
|
|
||||||
velocity_coverage_pct: 72,
|
|
||||||
confidence: "high",
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("DataQualityCard — нейтральный fallback (#2452)", () => {
|
|
||||||
it("известное значение рисуется своей меткой", () => {
|
|
||||||
render(<DataQualityCard dq={makeDq({ confidence: "low" })} />);
|
|
||||||
expect(screen.getByText("Низкое")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("значение вне union'а не роняет рендер и даёт «—»", () => {
|
|
||||||
const drifted = makeDq({
|
|
||||||
// Имитация schema drift: backend прислал значение вне
|
|
||||||
// "high"|"medium"|"low". Приведение через unknown, не через any.
|
|
||||||
confidence: "unverified" as unknown as DataQuality["confidence"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => render(<DataQualityCard dq={drifted} />)).not.toThrow();
|
|
||||||
expect(screen.getByText("—")).toBeInTheDocument();
|
|
||||||
// Покрытие продолжает рисоваться — падает только метка, не вся карточка.
|
|
||||||
expect(screen.getByText(/покрытие 72%/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fallback не выглядит семантически (не зелёный и не красный)", () => {
|
|
||||||
const drifted = makeDq({
|
|
||||||
confidence: "unverified" as unknown as DataQuality["confidence"],
|
|
||||||
});
|
|
||||||
render(<DataQualityCard dq={drifted} />);
|
|
||||||
|
|
||||||
const badge = screen.getByText("—");
|
|
||||||
const style = badge.getAttribute("style") ?? "";
|
|
||||||
expect(style).not.toContain("--success");
|
|
||||||
expect(style).not.toContain("--danger");
|
|
||||||
expect(style).not.toContain("--warn");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -167,8 +167,7 @@ function bandByHorizon(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-horizon confidence level for a forecast list (for the band tooltip). */
|
/** Per-horizon confidence level for a forecast list (for the band tooltip). */
|
||||||
// Экспорт ради теста (#2452), как и `isDeficitDegenerate` ниже.
|
function confidenceByHorizon(
|
||||||
export function confidenceByHorizon(
|
|
||||||
forecasts: DemandSupplyForecast[],
|
forecasts: DemandSupplyForecast[],
|
||||||
horizons: number[],
|
horizons: number[],
|
||||||
): (string | null)[] {
|
): (string | null)[] {
|
||||||
|
|
@ -176,10 +175,7 @@ export function confidenceByHorizon(
|
||||||
for (const f of forecasts) byH.set(f.horizon_months, f);
|
for (const f of forecasts) byH.set(f.horizon_months, f);
|
||||||
return horizons.map((h) => {
|
return horizons.map((h) => {
|
||||||
const f = byH.get(h);
|
const f = byH.get(h);
|
||||||
// `?? "—"` (#2452): значение вне закрытого union'а (schema drift /
|
return f == null ? null : CONFIDENCE_RU[f.confidence];
|
||||||
// частичный деплой) иначе вернёт undefined и подпись горизонта пропадёт
|
|
||||||
// молча — при том, что прогноз для него есть. Нейтральное «—» честнее.
|
|
||||||
return f == null ? null : (CONFIDENCE_RU[f.confidence] ?? "—");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/**
|
|
||||||
* #2452 (follow-up #2445-B2/B3) — `CONFIDENCE_RU[f.confidence]` в
|
|
||||||
* `confidenceByHorizon` не имел fallback'а. В отличие от соседних мест этого
|
|
||||||
* класса, здесь промах не роняет рендер: он возвращает `undefined`, и подпись
|
|
||||||
* уверенности для горизонта пропадает молча — при том, что прогноз для него
|
|
||||||
* есть. Тихая потеря подписи хуже, чем «—»: на графике не видно, что данные
|
|
||||||
* были.
|
|
||||||
*/
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { confidenceByHorizon } from "../ForecastChart";
|
|
||||||
import type { DemandSupplyForecast } from "@/types/forecast";
|
|
||||||
|
|
||||||
function makeForecast(
|
|
||||||
horizon: number,
|
|
||||||
confidence: string,
|
|
||||||
): DemandSupplyForecast {
|
|
||||||
return {
|
|
||||||
horizon_months: horizon,
|
|
||||||
deficit_index: 0.2,
|
|
||||||
rate_future: 12.5,
|
|
||||||
confidence: confidence as DemandSupplyForecast["confidence"],
|
|
||||||
} as DemandSupplyForecast;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("confidenceByHorizon — нейтральный fallback (#2452)", () => {
|
|
||||||
it("известные уровни переводятся", () => {
|
|
||||||
const out = confidenceByHorizon(
|
|
||||||
[makeForecast(12, "high"), makeForecast(24, "low")],
|
|
||||||
[12, 24],
|
|
||||||
);
|
|
||||||
expect(out).toEqual(["высокая", "низкая"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("горизонт без прогноза остаётся null (это не то же, что неизвестный уровень)", () => {
|
|
||||||
const out = confidenceByHorizon([makeForecast(12, "medium")], [12, 36]);
|
|
||||||
expect(out).toEqual(["средняя", null]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("значение вне union'а даёт «—», а не пропадает молча", () => {
|
|
||||||
// Имитация schema drift: backend прислал уровень вне "high"|"medium"|"low".
|
|
||||||
const out = confidenceByHorizon([makeForecast(12, "unverified")], [12]);
|
|
||||||
expect(out).toEqual(["—"]);
|
|
||||||
// Именно не undefined: undefined и null означали бы «прогноза нет», а он есть.
|
|
||||||
expect(out[0]).not.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("не бросает на смеси известных, неизвестных и отсутствующих", () => {
|
|
||||||
expect(() =>
|
|
||||||
confidenceByHorizon(
|
|
||||||
[makeForecast(12, "high"), makeForecast(24, "странное")],
|
|
||||||
[12, 24, 36],
|
|
||||||
),
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -172,10 +172,7 @@ const METRIC_ROWS: MetricRow[] = [
|
||||||
{c.confidenceValue != null
|
{c.confidenceValue != null
|
||||||
? `${Math.round(c.confidenceValue * 100)}% · `
|
? `${Math.round(c.confidenceValue * 100)}% · `
|
||||||
: ""}
|
: ""}
|
||||||
{/* `?? DASH` (#2452): confidenceLabel — строка из API, а не
|
{CONFIDENCE_RU[c.confidenceLabel]}
|
||||||
выведенный литерал; значение вне union'а (schema drift)
|
|
||||||
иначе печатает пустоту рядом с процентом. */}
|
|
||||||
{CONFIDENCE_RU[c.confidenceLabel] ?? DASH}
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
DASH
|
DASH
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/**
|
|
||||||
* #2452 (follow-up #2445-B2/B3) — `CONFIDENCE_RU[c.confidenceLabel]` в строке
|
|
||||||
* «Достоверность» не имел fallback'а. Промах union'а здесь не роняет рендер
|
|
||||||
* (в JSX `undefined` печатается пустотой), но даёт хуже: рядом с процентом
|
|
||||||
* остаётся «73% · » и обрыв — число выглядит недосказанным, а не неизвестным.
|
|
||||||
*
|
|
||||||
* Тест фиксирует «—» на значении вне union'а и присутствие процента рядом.
|
|
||||||
*/
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { CompareTable } from "../CompareTable";
|
|
||||||
import type { CompareColumn } from "../CompareTable";
|
|
||||||
|
|
||||||
function makeColumn(overrides: Partial<CompareColumn> = {}): CompareColumn {
|
|
||||||
return {
|
|
||||||
cad: "66:41:0704002:1",
|
|
||||||
status: "ready",
|
|
||||||
confidenceLabel: "high",
|
|
||||||
confidenceValue: 0.73,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("CompareTable — строка «Достоверность» (#2452)", () => {
|
|
||||||
it("известный уровень печатается словом", () => {
|
|
||||||
render(<CompareTable columns={[makeColumn()]} onRemove={() => {}} />);
|
|
||||||
expect(screen.getByText(/высокая/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("значение вне union'а даёт «—», а не обрыв после процента", () => {
|
|
||||||
const drifted = makeColumn({
|
|
||||||
// Имитация schema drift: значение вне "high"|"medium"|"low".
|
|
||||||
confidenceLabel:
|
|
||||||
"unverified" as unknown as CompareColumn["confidenceLabel"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
render(<CompareTable columns={[drifted]} onRemove={() => {}} />),
|
|
||||||
).not.toThrow();
|
|
||||||
// Процент остался на месте, а уровень — нейтральное «—» в той же ячейке.
|
|
||||||
expect(screen.getByText(/73% · —/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -79,9 +79,7 @@ function fmtPipeline(v: number | null | undefined): string {
|
||||||
: DASH;
|
: DASH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экспорт ради теста (#2452): строка «Достоверность» читает значение из API
|
const METRIC_ROWS: MetricRow[] = [
|
||||||
// по ключу закрытого union'а, и её нужно проверять на out-of-union прямо.
|
|
||||||
export const METRIC_ROWS: MetricRow[] = [
|
|
||||||
{
|
{
|
||||||
key: "verdict",
|
key: "verdict",
|
||||||
label: "Вердикт МКД",
|
label: "Вердикт МКД",
|
||||||
|
|
@ -150,10 +148,7 @@ export const METRIC_ROWS: MetricRow[] = [
|
||||||
{c.confidenceValue != null
|
{c.confidenceValue != null
|
||||||
? `${Math.round(c.confidenceValue * 100)}% · `
|
? `${Math.round(c.confidenceValue * 100)}% · `
|
||||||
: ""}
|
: ""}
|
||||||
{/* `?? DASH` (#2452): confidenceLabel — строка из API, а не
|
{CONFIDENCE_RU[c.confidenceLabel]}
|
||||||
выведенный литерал; значение вне union'а (schema drift)
|
|
||||||
иначе печатает пустоту рядом с процентом. */}
|
|
||||||
{CONFIDENCE_RU[c.confidenceLabel] ?? DASH}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
DASH
|
DASH
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/**
|
|
||||||
* #2452 (follow-up #2445-B2/B3) — тот же незащищённый enum-lookup, что в
|
|
||||||
* `compare/CompareTable.tsx`: строка «Достоверность» читает
|
|
||||||
* `CONFIDENCE_RU[c.confidenceLabel]` по значению из API. Промах union'а
|
|
||||||
* печатал пустоту сразу после процента («73% · »), то есть число выглядело
|
|
||||||
* недосказанным, а не неизвестным.
|
|
||||||
*
|
|
||||||
* Проверяем саму строку METRIC_ROWS, а не всю страницу: `PticaCompare` тянет
|
|
||||||
* данные по `cad`, и оборачивать его в query-моки ради одной ячейки — лишний
|
|
||||||
* тестовый вес при том же покрытии.
|
|
||||||
*/
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { METRIC_ROWS } from "../PticaCompare";
|
|
||||||
import type { CompareColumn } from "../../../compare/CompareTable";
|
|
||||||
|
|
||||||
const confidenceRow = METRIC_ROWS.find((r) => r.key === "confidence");
|
|
||||||
|
|
||||||
function makeColumn(overrides: Partial<CompareColumn> = {}): CompareColumn {
|
|
||||||
return {
|
|
||||||
cad: "66:41:0704002:1",
|
|
||||||
status: "ready",
|
|
||||||
confidenceLabel: "medium",
|
|
||||||
confidenceValue: 0.73,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("PticaCompare — строка «Достоверность» (#2452)", () => {
|
|
||||||
it("строка на месте (иначе тест ниже зеленел бы впустую)", () => {
|
|
||||||
expect(confidenceRow).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("известный уровень печатается словом", () => {
|
|
||||||
render(<>{confidenceRow!.render(makeColumn())}</>);
|
|
||||||
expect(screen.getByText(/средняя/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("значение вне union'а даёт «—», а не обрыв после процента", () => {
|
|
||||||
const drifted = makeColumn({
|
|
||||||
confidenceLabel:
|
|
||||||
"unverified" as unknown as CompareColumn["confidenceLabel"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => render(<>{confidenceRow!.render(drifted)}</>)).not.toThrow();
|
|
||||||
expect(screen.getByText(/73% · —/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Add table
Reference in a new issue