Fix map calendar relative date labels
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user