Wire first-entry geolocation into Schedule start page (TZ 4.1.1-R8)

This commit is contained in:
2026-04-21 19:11:57 +03:00
parent b023cb922a
commit c3c1f830b9
2 changed files with 82 additions and 0 deletions
@@ -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(<ScheduleStartPage />);
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(<ScheduleStartPage />);
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(<ScheduleStartPage />);
// 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(<ScheduleStartPage />);
const timeSelector = screen.getByTestId("time-selector");
expect(timeSelector.textContent).toContain("00:00");
expect(timeSelector.textContent).toContain("24:00");
});
});
@@ -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<string>(prefill.departure ?? "");
const [arrivalCode, setArrivalCode] = useState<string>(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