Fix map calendar relative date labels

This commit is contained in:
2026-05-06 14:38:17 +03:00
parent 385a6e55ee
commit 65e776273d
2 changed files with 154 additions and 2 deletions
@@ -2,7 +2,7 @@
* @vitest-environment jsdom * @vitest-environment jsdom
*/ */
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { FlightsMapFilter } from "./FlightsMapFilter.js"; import { FlightsMapFilter } from "./FlightsMapFilter.js";
import type { IFlightsMapFilterState } from "../types.js"; import type { IFlightsMapFilterState } from "../types.js";
@@ -40,7 +40,12 @@ vi.mock("@modern-js/runtime/router", () => ({
vi.mock("@/i18n/provider.js", () => ({ vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({ useTranslation: () => ({
t: (key: string) => key, t: (key: string) =>
key === "SHARED.TODAY"
? "Сегодня"
: key === "SHARED.TOMORROW"
? "Завтра"
: key,
i18n: { language: "ru" }, i18n: { language: "ru" },
}), }),
})); }));
@@ -201,6 +206,83 @@ describe("FlightsMapFilter — Calendar wiring", () => {
}); });
}); });
describe("FlightsMapFilter — TIRREDESIGN-22: calendar today/tomorrow labels", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 4, 6, 12, 0, 0));
lastCalendarProps = null;
});
afterEach(() => {
vi.useRealTimers();
});
it("formats today's selected date as a word", () => {
render(
<FlightsMapFilter
value={filter({ departure: "MOW", date: "20260506" })}
onChange={vi.fn()}
/>,
);
const formatDateTime = lastCalendarProps!["formatDateTime"] as (
date: Date,
) => string;
expect(formatDateTime(new Date(2026, 4, 6))).toBe("Сегодня");
});
it("formats tomorrow's selected date as a word", () => {
render(
<FlightsMapFilter
value={filter({ departure: "MOW", date: "20260507" })}
onChange={vi.fn()}
/>,
);
const formatDateTime = lastCalendarProps!["formatDateTime"] as (
date: Date,
) => string;
expect(formatDateTime(new Date(2026, 4, 7))).toBe("Завтра");
});
it("keeps normal dates in dd.MM.yyyy format", () => {
render(
<FlightsMapFilter
value={filter({ departure: "MOW", date: "20260508" })}
onChange={vi.fn()}
/>,
);
const formatDateTime = lastCalendarProps!["formatDateTime"] as (
date: Date,
) => string;
expect(formatDateTime(new Date(2026, 4, 8))).toBe("08.05.2026");
});
it("parses typed today/tomorrow words back to dates", () => {
render(
<FlightsMapFilter
value={filter({ departure: "MOW", date: "20260506" })}
onChange={vi.fn()}
/>,
);
const parseDateTime = lastCalendarProps!["parseDateTime"] as (
text: string,
) => Date;
expect(parseDateTime("Сегодня").toISOString()).toBe(
new Date(2026, 4, 6).toISOString(),
);
expect(parseDateTime("Завтра").toISOString()).toBe(
new Date(2026, 4, 7).toISOString(),
);
});
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// TZ §4.1.24.2 R16: Calendar disabled when Город вылета is empty // TZ §4.1.24.2 R16: Calendar disabled when Город вылета is empty
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -44,6 +44,64 @@ function dateToYyyymmdd(value: Date): string {
return `${y}${m}${d}`; return `${y}${m}${d}`;
} }
function addDays(base: Date, days: number): Date {
const date = new Date(base);
date.setDate(date.getDate() + days);
date.setHours(0, 0, 0, 0);
return date;
}
function isSameDay(a: Date, b: Date): boolean {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function formatDateInputValue(value: Date, t: (key: string) => string): string {
const today = new Date();
today.setHours(0, 0, 0, 0);
if (isSameDay(value, today)) return t("SHARED.TODAY");
if (isSameDay(value, addDays(today, 1))) return t("SHARED.TOMORROW");
const d = value.getDate().toString().padStart(2, "0");
const m = (value.getMonth() + 1).toString().padStart(2, "0");
return `${d}.${m}.${value.getFullYear()}`;
}
function parseDateInputValue(value: string, t: (key: string) => string): Date | null {
const text = value.trim();
const today = new Date();
today.setHours(0, 0, 0, 0);
if (text.localeCompare(t("SHARED.TODAY"), undefined, { sensitivity: "accent" }) === 0) {
return today;
}
if (
text.localeCompare(t("SHARED.TOMORROW"), undefined, { sensitivity: "accent" }) === 0
) {
return addDays(today, 1);
}
const match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(text);
if (!match) return null;
const day = Number(match[1]);
const month = Number(match[2]) - 1;
const year = Number(match[3]);
const date = new Date(year, month, day);
date.setHours(0, 0, 0, 0);
return date.getFullYear() === year &&
date.getMonth() === month &&
date.getDate() === day
? date
: null;
}
/** /**
* Filter component for the flights map. Controls departure, arrival, * Filter component for the flights map. Controls departure, arrival,
* connections, domestic/international toggles, and date selection. * connections, domestic/international toggles, and date selection.
@@ -153,6 +211,16 @@ export const FlightsMapFilter: FC<FlightsMapFilterProps> = ({
[value, onChange], [value, onChange],
); );
const formatDateTime = useCallback(
(date: Date) => formatDateInputValue(date, t),
[t],
);
const parseDateTime = useCallback(
(text: string) => parseDateInputValue(text, t) ?? new Date(Number.NaN),
[t],
);
return ( return (
<div className="flights-map-filter" data-testid="flights-map-filter"> <div className="flights-map-filter" data-testid="flights-map-filter">
<div className="flights-map-filter-header"> <div className="flights-map-filter-header">
@@ -294,6 +362,8 @@ export const FlightsMapFilter: FC<FlightsMapFilterProps> = ({
maxDate={maxDate} maxDate={maxDate}
disabledDates={disabledDates} disabledDates={disabledDates}
dateFormat="dd.mm.yy" dateFormat="dd.mm.yy"
formatDateTime={formatDateTime}
parseDateTime={parseDateTime}
placeholder={t("SHARED.DATE_FORMAT")} placeholder={t("SHARED.DATE_FORMAT")}
showIcon showIcon
disabled={!value.departure} disabled={!value.departure}