diff --git a/src/features/flights-map/components/FlightsMapFilter.test.tsx b/src/features/flights-map/components/FlightsMapFilter.test.tsx new file mode 100644 index 00000000..c95127ed --- /dev/null +++ b/src/features/flights-map/components/FlightsMapFilter.test.tsx @@ -0,0 +1,172 @@ +/** + * @vitest-environment jsdom + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render } from "@testing-library/react"; +import { FlightsMapFilter } from "./FlightsMapFilter.js"; +import type { IFlightsMapFilterState } from "../types.js"; + +// Capture props passed to PrimeReact Calendar. +let lastCalendarProps: Record | null = null; +vi.mock("primereact/calendar", () => ({ + Calendar: (props: Record) => { + lastCalendarProps = props; + return
; + }, +})); + +// Stub PrimeReact AutoComplete so rendering is cheap. +vi.mock("primereact/autocomplete", () => ({ + AutoComplete: (props: Record) => ( + + ), +})); + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: "ru" }, + }), +})); + +vi.mock("@/shared/hooks/useCitySearch.js", () => ({ + useCitySearch: () => ({ suggestions: [], search: vi.fn() }), +})); + +function filter( + overrides: Partial = {}, +): IFlightsMapFilterState { + return { + connections: false, + domestic: false, + international: false, + ...overrides, + }; +} + +function yyyymmdd(d: Date): string { + const y = d.getFullYear().toString(); + const m = (d.getMonth() + 1).toString().padStart(2, "0"); + const day = d.getDate().toString().padStart(2, "0"); + return `${y}${m}${day}`; +} + +function addDays(base: Date, n: number): Date { + const d = new Date(base); + d.setDate(d.getDate() + n); + d.setHours(0, 0, 0, 0); + return d; +} + +describe("FlightsMapFilter — Calendar wiring", () => { + beforeEach(() => { + lastCalendarProps = null; + }); + + it("passes minDate and maxDate to Calendar", () => { + const onChange = vi.fn(); + render(); + + const min = lastCalendarProps!["minDate"] as Date; + const max = lastCalendarProps!["maxDate"] as Date; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const expectedMin = addDays(today, -1); + const expectedMax = new Date(today); + expectedMax.setMonth(expectedMax.getMonth() + 6); + + expect(min.getTime()).toBe(expectedMin.getTime()); + expect(max.getTime()).toBe(expectedMax.getTime()); + }); + + it("disables every date when availableDays is empty", () => { + const onChange = vi.fn(); + render( + , + ); + + const disabled = lastCalendarProps!["disabledDates"] as Date[]; + expect(disabled.length).toBeGreaterThan(180); + }); + + it("excludes available days from disabledDates", () => { + const onChange = vi.fn(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const plusTwo = addDays(today, 2); + + render( + , + ); + + const disabled = lastCalendarProps!["disabledDates"] as Date[]; + const contains = disabled.some((d) => { + const x = new Date(d); + x.setHours(0, 0, 0, 0); + return x.getTime() === plusTwo.getTime(); + }); + expect(contains).toBe(false); + }); + + it("snaps a disabled value.date forward to the next available day", () => { + const onChange = vi.fn(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const plusOne = addDays(today, 1); + const plusTwo = addDays(today, 2); + + render( + , + ); + + expect(onChange).toHaveBeenCalled(); + const called = onChange.mock.calls[0]![0] as IFlightsMapFilterState; + expect(called.date).toBe(yyyymmdd(plusTwo)); + }); + + it("clears value.date when no enabled date exists in the window", () => { + const onChange = vi.fn(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const plusOne = addDays(today, 1); + + render( + , + ); + + expect(onChange).toHaveBeenCalled(); + const called = onChange.mock.calls[0]![0] as IFlightsMapFilterState; + expect(called.date).toBeUndefined(); + }); + + it("does not snap when the current date is already enabled", () => { + const onChange = vi.fn(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const plusTwo = addDays(today, 2); + + render( + , + ); + + expect(onChange).not.toHaveBeenCalled(); + }); +});