diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx index d3c552f9..0ec5d5cf 100644 --- a/src/features/schedule/components/ScheduleStartPage.test.tsx +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -91,10 +91,29 @@ vi.mock("@/shared/dictionaries/index.js", () => ({ getCityCodeByAirportCode: () => undefined, })); +// Capture the latest onCity callback so individual tests can invoke it. +let capturedOnCity: ((code: string) => void) | null = null; +let capturedShouldApply: (() => boolean) | null = null; +let geoMockEnabled = false; + +vi.mock("@/shared/hooks/useGeoCityDefault.js", () => ({ + useGeoCityDefault: (opts: { shouldApply: () => boolean; onCity: (code: string) => void }) => { + capturedOnCity = opts.onCity; + capturedShouldApply = opts.shouldApply; + // If the test enabled geo, fire it synchronously so we don't need async. + if (geoMockEnabled && opts.shouldApply()) { + opts.onCity("MOW"); + } + }, +})); + describe("ScheduleStartPage", () => { beforeEach(() => { vi.clearAllMocks(); sessionStore.clear(); + geoMockEnabled = false; + capturedOnCity = null; + capturedShouldApply = null; }); it("renders the start page", () => { @@ -180,3 +199,53 @@ describe("4.1.8: Schedule hydrates from cross-section store on mount", () => { expect(depInput.defaultValue).toBe(""); }); }); + +// --------------------------------------------------------------------------- +// TZ §4.1.1-R8/R10: first-entry geolocation auto-fill + time default +// --------------------------------------------------------------------------- + +describe("4.1.1-R8/R10: Schedule first-entry geolocation + time default", () => { + beforeEach(() => { + vi.clearAllMocks(); + sessionStore.clear(); + resetCrossSectionStore(); + geoMockEnabled = false; + capturedOnCity = null; + capturedShouldApply = null; + }); + + it("4.1.1-R8: populates departure with geolocated city on first entry", () => { + geoMockEnabled = true; // mock will call onCity("MOW") synchronously on shouldApply() + render(); + const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement; + expect(depInput.defaultValue).toBe("MOW"); + }); + + it("4.1.1-R8: does not populate when geolocation denied (geoMockEnabled=false)", () => { + // geoMockEnabled stays false → useGeoCityDefault mock never fires onCity + render(); + const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement; + expect(depInput.defaultValue).toBe(""); + }); + + it("4.1.1-R8: does not override departure when cross-section store has schedule filter", () => { + setScheduleFilter({ + mode: "route", departure: "LED", arrival: "KGD", + dateFrom: "20260515", dateTo: "20260521", + timeFrom: "0000", timeTo: "2400", + onlyDirect: false, showReturn: false, searchExecuted: false, + }); + geoMockEnabled = true; + render(); + // Departure should be LED from the snapshot, NOT overridden by geo. + const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement; + expect(depInput.defaultValue).toBe("LED"); + }); + + it("4.1.1-R10: time default = 00:00-24:00 on all viewports", () => { + render(); + const timeSelector = screen.getByTestId("time-selector"); + expect(timeSelector.textContent).toContain("00:00"); + expect(timeSelector.textContent).toContain("24:00"); + }); +}); diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx index 8da32ea3..d453c47c 100644 --- a/src/features/schedule/components/ScheduleStartPage.tsx +++ b/src/features/schedule/components/ScheduleStartPage.tsx @@ -39,6 +39,7 @@ import { getCityCodeByAirportCode, } from "@/shared/dictionaries/index.js"; import type { IDictionaries } from "@/shared/dictionaries/index.js"; +import { useGeoCityDefault } from "@/shared/hooks/useGeoCityDefault.js"; import { buildScheduleUrl } from "../url.js"; import { scheduleWindowBounds } from "@/shared/dateWindow.js"; import "./ScheduleStartPage.scss"; @@ -136,6 +137,18 @@ export const ScheduleStartPage: FC = () => { const [departureCode, setDepartureCode] = useState(prefill.departure ?? ""); const [arrivalCode, setArrivalCode] = useState(prefill.arrival ?? ""); + // TZ §4.1.1-R8: on first session entry with geo consent, auto-fill + // departure city with the user's nearest city when: + // - no stored schedule filter is present (fresh session), AND + // - the current departure is empty (not populated by prefill or user). + useGeoCityDefault({ + dictionaries, + shouldApply: () => !getScheduleFilter() && !departureCode, + onCity: (cityCode) => { + setDepartureCode((prev) => (prev ? prev : cityCode)); + }, + }); + // Start blank to match Angular's `ДД.ММ.ГГГГ - ДД.ММ.ГГГГ` placeholder // (the "current week" pre-fill was a React-only convenience that // pulled the date input out of parity). Submit handler defaults to