117 lines
6.9 KiB
TypeScript
117 lines
6.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { activityLevel, freshnessStatus, passportDisplayStatus, plural, sourceStatusLabel } from "../../src/lib/presentation.ts";
|
|
import { fishVisualFamily } from "../../src/lib/fish-visuals.ts";
|
|
import { tackleVisualKind, tackleVisualTone } from "../../src/lib/tackle-visuals.ts";
|
|
import { waterbodyVisual } from "../../src/lib/waterbody-visuals.ts";
|
|
import { pageHref, pageWindow } from "../../src/lib/pagination.ts";
|
|
import { adminEndsSession, adminErrorMessage } from "../../src/lib/admin-errors.ts";
|
|
import { findMediaByLabel, mediaRoleLabel } from "../../src/lib/media.ts";
|
|
|
|
test("activity levels share one complete 0-100 scale", () => {
|
|
assert.deepEqual(
|
|
[0, 39, 40, 59, 60, 79, 80, 100].map(score => activityLevel(score).short),
|
|
["Низкий", "Низкий", "Средний", "Средний", "Высокий", "Высокий", "Очень высокий", "Очень высокий"],
|
|
);
|
|
});
|
|
|
|
test("Russian plural forms cover common and exceptional endings", () => {
|
|
const forms = ["точка", "точки", "точек"] as const;
|
|
assert.deepEqual(
|
|
[0, 1, 2, 5, 11, 21, 22, 25].map(number => plural(number, forms)),
|
|
["точек", "точка", "точки", "точек", "точек", "точка", "точки", "точек"],
|
|
);
|
|
});
|
|
|
|
test("freshness status is explicit and deterministic around the 48-hour boundary", () => {
|
|
const now = Date.parse("2026-09-20T12:00:00Z");
|
|
assert.equal(freshnessStatus("2026-09-20T11:59:00Z", now), "fresh");
|
|
assert.equal(freshnessStatus("2026-09-18T11:59:00Z", now), "stale");
|
|
assert.equal(freshnessStatus(null, now), "unknown");
|
|
});
|
|
|
|
test("evidence passport prioritises conflict and stale states without hiding blocked data", () => {
|
|
assert.equal(passportDisplayStatus("verified", "fresh", false), "verified");
|
|
assert.equal(passportDisplayStatus("verified", "stale", false), "stale");
|
|
assert.equal(passportDisplayStatus("blocked", "fresh", false), "blocked");
|
|
assert.equal(passportDisplayStatus("incomplete", "fresh", true), "conflict");
|
|
});
|
|
|
|
test("source status makes a blocked provider explicit", () => {
|
|
assert.equal(sourceStatusLabel("temporarily_limited"), "Источник временно ограничен");
|
|
assert.equal(sourceStatusLabel("healthy"), "Актуален");
|
|
assert.equal(sourceStatusLabel("unexpected"), "Состояние не определено");
|
|
});
|
|
|
|
test("fish visual families follow body type instead of a name hash", () => {
|
|
assert.deepEqual(
|
|
["Щука", "Озёрная форель", "Карп", "Окунь", "Сом", "Угорь", "Палтус", "Треска", "Неизвестная рыба"].map(fishVisualFamily),
|
|
["pike", "salmonid", "cyprinid", "perch", "catfish", "eel", "flatfish", "marine", "generic"],
|
|
);
|
|
});
|
|
|
|
test("tackle glyphs only infer a type from explicit name terms", () => {
|
|
assert.deepEqual(
|
|
["Spiker #2", "Minnow 80F", "Salmon T1 Shad", "Клубничный бойл", "Червь", "Карповый монтаж", "Model 42", null].map(tackleVisualKind),
|
|
["spinner", "wobbler", "soft", "boilie", "worm", "rig", "unknown", "unknown"],
|
|
);
|
|
assert.equal(tackleVisualTone("Spiker #2"), tackleVisualTone("spiker #2"));
|
|
});
|
|
|
|
test("waterbody fingerprints are stable, bounded and entity-specific", () => {
|
|
const kuori = waterbodyVisual("kuori");
|
|
assert.deepEqual(kuori, waterbodyVisual(" KUORI "));
|
|
assert.notDeepEqual(kuori, waterbodyVisual("vyunok"));
|
|
assert.ok(kuori.shore >= 0 && kuori.shore < 8);
|
|
assert.ok(kuori.waves >= 1 && kuori.waves <= 3);
|
|
assert.ok(kuori.markerX >= 32 && kuori.markerX <= 90);
|
|
assert.ok(kuori.markerY >= 18 && kuori.markerY <= 40);
|
|
assert.match(kuori.code, /^[0-9A-Z]{2}$/);
|
|
});
|
|
|
|
test("waterbody media lookup only accepts reviewed roles when constrained", () => {
|
|
const assets = [
|
|
{ id: "screenshot", entity_type: "waterbody", entity_key: "kuori", media_role: "waterbody_screenshot", label: "Куори", width: 1, height: 1, content_type: "image/webp", image_url: "", source_system: "rf4db", source_url: "" },
|
|
{ id: "map", entity_type: "waterbody", entity_key: "kuori", media_role: "waterbody_map", label: "Куори", width: 1, height: 1, content_type: "image/webp", image_url: "", source_system: "rf4db", source_url: "" },
|
|
] as const;
|
|
assert.equal(findMediaByLabel([...assets], "Куори", ["waterbody_cover", "waterbody_map"])?.id, "map");
|
|
assert.equal(findMediaByLabel([...assets], "Куори", ["waterbody_cover"]), undefined);
|
|
});
|
|
|
|
test("reviewed media roles keep human-readable labels with a safe fallback", () => {
|
|
assert.deepEqual(
|
|
["waterbody_cover", "waterbody_map", "waterbody_depth_map", "waterbody_screenshot", "tackle_card", "tackle_detail", "rig_diagram", "tackle_screenshot"].map(mediaRoleLabel),
|
|
["Заставка водоёма", "Карта водоёма", "Карта глубин", "Скриншот водоёма", "Карточка снасти", "Деталь снасти", "Схема монтажа", "Скриншот снасти"],
|
|
);
|
|
assert.equal(mediaRoleLabel("future_role"), "future_role");
|
|
});
|
|
|
|
test("45 records are reachable across three stable pages", () => {
|
|
const pages = [pageWindow(45, 20, 0), pageWindow(45, 20, 20), pageWindow(45, 20, 40)];
|
|
assert.deepEqual(pages.map(page => [page.start, page.end]), [[1, 20], [21, 40], [41, 45]]);
|
|
assert.deepEqual(pages.map(page => [page.previousOffset, page.nextOffset]), [[null, 20], [0, 40], [20, null]]);
|
|
assert.deepEqual(pages.map(page => page.page), [1, 2, 3]);
|
|
const reached = pages.flatMap(page => Array.from({ length: page.end - page.start + 1 }, (_, index) => page.start + index));
|
|
assert.deepEqual(reached, Array.from({ length: 45 }, (_, index) => index + 1));
|
|
});
|
|
|
|
test("pagination links preserve filters and replace offset", () => {
|
|
const params = new URLSearchParams("fish=pike&waterbody=kuori&offset=20&offset=999");
|
|
const next = pageHref("/records", params, 40);
|
|
const first = pageHref("/records", params, 0, "#results");
|
|
assert.equal(next, "/records?fish=pike&waterbody=kuori&offset=40");
|
|
assert.equal(first, "/records?fish=pike&waterbody=kuori#results");
|
|
});
|
|
|
|
test("admin errors keep auth and operational responses consistent", () => {
|
|
assert.equal(adminErrorMessage(401, "fallback"), "Неверный или истёкший административный токен.");
|
|
assert.equal(adminErrorMessage(409, "fallback"), "Операция конфликтует с изменением в другой вкладке.");
|
|
assert.equal(adminErrorMessage(429, "fallback"), "Слишком много попыток. Повторите позже.");
|
|
assert.equal(adminErrorMessage(503, "fallback"), "Сервис временно недоступен. Проверьте состояние и повторите позже.");
|
|
assert.equal(adminErrorMessage(422, "fallback"), "fallback");
|
|
assert.equal(adminEndsSession(401), true);
|
|
assert.equal(adminEndsSession(429), true);
|
|
assert.equal(adminEndsSession(409), false);
|
|
});
|