Per-section history cap (8) + rename 'Вы искали' → 'Ранее искали' (TIRREDESIGN-5)

Angular keeps up to 8 items in each sidebar section (board / schedule
/ flight-number). We were capping the union at 10, which let a burst of
flight-number lookups evict board-route entries. Split the cap by
section so each bucket is independent.

Label also moves from 'Вы искали' → 'Ранее искали' (en: 'Previous
searches') per the redesign copy. Tests cover both the single-section
cap and the independence invariant.
This commit is contained in:
2026-04-22 13:45:30 +03:00
parent a26adad895
commit 8bde3904e1
4 changed files with 70 additions and 10 deletions
+2 -2
View File
@@ -36,7 +36,7 @@
"STATUS-PLANNED": "Scheduled",
"TIME_DEPARTURE": "Departure time",
"TITLE": "Online Timetable",
"YOU_SEARCH": "You searched",
"YOU_SEARCH": "Previous searches",
"LEG": "Leg",
"TOTAL-FLYING-TIME": "Total flying time",
"DETAILS-TITLE": "Flight details",
@@ -316,7 +316,7 @@
"BACK-SCHEDULE": "Back to Schedule",
"BAGBELT": "Baggage Carousel Number",
"BOARD_SCHEDULE": "Online Timetable and Schedule",
"BUY-TICKET": "Buy",
"BUY-TICKET": "Buy a ticket",
"BUY-TICKET-HIT-RATE": "Buy a ticket at the Hit Fare",
"CITY_CHANGE": "Different cities",
"CITY_PLACEHOLDER": "Specify city",
+2 -2
View File
@@ -36,7 +36,7 @@
"STATUS-PLANNED": "Запланирован",
"TIME_DEPARTURE": "Время отправления",
"TITLE": "Онлайн-Табло",
"YOU_SEARCH": "Вы искали",
"YOU_SEARCH": "Ранее искали",
"LEG": "Перелет",
"TOTAL-FLYING-TIME": "Общее время в пути",
"DETAILS-TITLE": "Детали рейса",
@@ -316,7 +316,7 @@
"BACK-SCHEDULE": "Вернуться к Расписанию",
"BAGBELT": "Номер ленты выдачи багажа",
"BOARD_SCHEDULE": "Онлайн-Табло и Расписание",
"BUY-TICKET": "Купить",
"BUY-TICKET": "Купить билет",
"BUY-TICKET-HIT-RATE": "Купить билет по Хит тарифу",
"CITY_CHANGE": "Смена города",
"CITY_PLACEHOLDER": "Укажите город",
+40 -3
View File
@@ -143,7 +143,10 @@ describe("useSearchHistory", () => {
expect(result.current.items).toEqual([flightItem, routeItem]);
});
it("limits history to 10 items", () => {
// TIRREDESIGN-5 / TZ §4.1.14.3.8: flight-number history keeps at most 8
// entries independently of board / schedule sections (one section
// filling up must not evict entries from the others).
it("caps flight-number section at 8 most-recent items", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
for (let i = 0; i < 12; i++) {
@@ -156,9 +159,43 @@ describe("useSearchHistory", () => {
});
}
expect(result.current.items).toHaveLength(10);
// Most recent should be first
expect(result.current.items).toHaveLength(8);
expect(result.current.items[0]?.url).toBe("/flight-11");
expect(result.current.items[7]?.url).toBe("/flight-4");
});
it("caps each section independently (board + schedule + flight-number)", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
for (let i = 0; i < 10; i++) {
act(() => {
result.current.add({
type: "flight-number",
url: `/flight-${i}`,
label: `Flight ${i}`,
});
});
act(() => {
result.current.add({
type: "board-route",
url: `/departure-${i}`,
label: `Departure ${i}`,
});
});
act(() => {
result.current.add({
type: "schedule-route",
url: `/schedule-${i}`,
label: `Schedule ${i}`,
});
});
}
const byType = (t: string) =>
result.current.items.filter((item) => item.type === t);
expect(byType("flight-number")).toHaveLength(8);
expect(byType("board-route")).toHaveLength(8);
expect(byType("schedule-route")).toHaveLength(8);
});
it("namespaces by language — different langs have separate history", () => {
+26 -3
View File
@@ -106,7 +106,16 @@ const searchHistorySchema = z.array(searchHistoryItemSchema);
// Storage key
// ---------------------------------------------------------------------------
const MAX_HISTORY_ITEMS = 10;
// TIRREDESIGN-5 / TZ §4.1.8: each section (board vs schedule vs flight-number)
// retains up to 8 recent searches independently. A new board-route entry
// cannot push a schedule-route entry out of the list.
const MAX_PER_SECTION = 8;
function sectionFor(type: SearchHistoryItem["type"]): "board" | "schedule" | "flight-number" {
if (type === "schedule-route") return "schedule";
if (type === "flight-number") return "flight-number";
return "board";
}
function storageKey(lang: string): string {
return `history_${lang}`;
@@ -153,9 +162,23 @@ export function useSearchHistory(lang: string): UseSearchHistoryResult {
return prev;
}
// Remove any existing entry with same URL, then prepend
// Remove any existing entry with the same URL, then prepend.
const filtered = prev.filter((existing) => existing.url !== item.url);
const next = [item, ...filtered].slice(0, MAX_HISTORY_ITEMS);
const combined = [item, ...filtered];
// Cap per section — the board/schedule/flight-number buckets each
// hold up to MAX_PER_SECTION (8) entries independently. This keeps
// the resulting list ordered by recency across sections but
// prevents a burst of board searches from evicting schedule
// history (and vice versa).
const perSectionCount = { board: 0, schedule: 0, "flight-number": 0 };
const next: SearchHistoryItem[] = [];
for (const entry of combined) {
const bucket = sectionFor(entry.type);
if (perSectionCount[bucket] >= MAX_PER_SECTION) continue;
perSectionCount[bucket]++;
next.push(entry);
}
sessionStore.set(key, next, searchHistorySchema);
if (typeof window !== "undefined") {