Align Flight-Map first-entry toggle defaults with TZ 4.1.1-R14/R21

This commit is contained in:
2026-04-21 19:22:16 +03:00
parent 4b6cb5bc40
commit fbb84fc0da
3 changed files with 292 additions and 13 deletions
@@ -2,7 +2,7 @@
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import { FlightsMapStartPage } from "./FlightsMapStartPage.js";
import type * as DictionariesModuleNS from "@/shared/dictionaries/index.js";
@@ -578,3 +578,212 @@ describe("4.1.8 / 4.1.1-R26: Flight-Map filter cross-section isolation", () => {
expect(filterValue["international"]).toBe(false);
});
});
// ---------------------------------------------------------------------------
// TZ §4.1.1 ¶4 / ¶9: Flight-Map first-entry toggle defaults
// ---------------------------------------------------------------------------
describe("4.1.1-R14/R21: Flight-Map first-entry toggle defaults per TZ §4.1.1", () => {
let originalGeolocation: Geolocation | undefined;
beforeEach(() => {
lastMapFilterProps = null;
dictState.dictionaries = buildDictionaries({
regions: [],
countries: [],
cities: [
{
code: "MOW",
title: { ru: "Москва" },
country_code: "RU",
has_afl_flights: true,
location: { lat: 55.75, lon: 37.62 },
},
],
airports: [
{
code: "SVO",
city_code: "MOW",
title: { ru: "Шереметьево" },
has_afl_flights: true,
location: { lat: 55.75, lon: 37.62 },
},
],
});
dictState.loading = false;
dictState.error = null;
searchState.routes = [];
searchState.loading = false;
searchState.error = null;
resetCrossSectionStore();
// Save original geolocation
originalGeolocation = navigator.geolocation;
});
afterEach(() => {
// Restore geolocation
Object.defineProperty(navigator, "geolocation", {
configurable: true,
writable: true,
value: originalGeolocation,
});
});
it("4.1.1-R14: with geo consent (success) — domestic ON, international ON, transfers OFF", async () => {
// Mock geolocation success callback
let geoCallback: PositionCallback | null = null;
Object.defineProperty(navigator, "geolocation", {
configurable: true,
writable: true,
value: {
getCurrentPosition: (cb: PositionCallback) => {
geoCallback = cb;
// Simulate async position callback on next tick
setTimeout(() => {
cb({
coords: {
latitude: 55.75,
longitude: 37.62,
accuracy: 50,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
},
timestamp: Date.now(),
} as GeolocationPosition);
}, 0);
},
},
});
const { rerender } = render(<FlightsMapStartPage />);
// Wait for geolocation callback to fire
await new Promise((resolve) => setTimeout(resolve, 50));
rerender(<FlightsMapStartPage />);
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
expect(filterValue["domestic"]).toBe(true); // R14: domestic ON
expect(filterValue["international"]).toBe(true); // R14: international ON
expect(filterValue["connections"]).toBe(false); // R14: transfers OFF
});
it("4.1.1-R21: without geo consent (error/denied) — all three toggles OFF", async () => {
// Mock geolocation failure (permission denied, etc.)
Object.defineProperty(navigator, "geolocation", {
configurable: true,
writable: true,
value: {
getCurrentPosition: (
_cb: PositionCallback,
errorCallback: PositionErrorCallback,
) => {
// Simulate async error callback on next tick
setTimeout(() => {
errorCallback({
code: 1, // PERMISSION_DENIED
message: "User denied geolocation",
PERMISSION_DENIED: 1,
POSITION_UNAVAILABLE: 2,
TIMEOUT: 3,
} as GeolocationPositionError);
}, 0);
},
},
});
const { rerender } = render(<FlightsMapStartPage />);
// Wait for geolocation error callback to fire
await new Promise((resolve) => setTimeout(resolve, 50));
rerender(<FlightsMapStartPage />);
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
expect(filterValue["domestic"]).toBe(false); // R21: domestic OFF
expect(filterValue["international"]).toBe(false); // R21: international OFF
expect(filterValue["connections"]).toBe(false); // R21: transfers OFF
});
it("R14: toggling off domestic when geo succeeds should not re-enable international", async () => {
// Mock geolocation success
Object.defineProperty(navigator, "geolocation", {
configurable: true,
writable: true,
value: {
getCurrentPosition: (cb: PositionCallback) => {
setTimeout(() => {
cb({
coords: {
latitude: 55.75,
longitude: 37.62,
accuracy: 50,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
},
timestamp: Date.now(),
} as GeolocationPosition);
}, 0);
},
},
});
const { rerender } = render(<FlightsMapStartPage />);
await new Promise((resolve) => setTimeout(resolve, 50));
rerender(<FlightsMapStartPage />);
// At this point, geo should have fired, setting both domestic and international to true
let filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
expect(filterValue["domestic"]).toBe(true);
expect(filterValue["international"]).toBe(true);
});
it("R21: stored map snapshot takes precedence over geo defaults", async () => {
// Pre-set a map snapshot with specific toggle values
const mapSnap: MapFilterSnapshot = {
departure: "MOW",
arrival: null,
date: null,
showInternal: false,
showInternational: true,
showTransfers: false,
};
setMapFilter(mapSnap);
// Mock geolocation success (should NOT override the snapshot)
Object.defineProperty(navigator, "geolocation", {
configurable: true,
writable: true,
value: {
getCurrentPosition: (cb: PositionCallback) => {
setTimeout(() => {
cb({
coords: {
latitude: 55.75,
longitude: 37.62,
accuracy: 50,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
},
timestamp: Date.now(),
} as GeolocationPosition);
}, 0);
},
},
});
const { rerender } = render(<FlightsMapStartPage />);
await new Promise((resolve) => setTimeout(resolve, 50));
rerender(<FlightsMapStartPage />);
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
// Snapshot values should be preserved, not overwritten by geo defaults
expect(filterValue["domestic"]).toBe(false);
expect(filterValue["international"]).toBe(true);
});
});
@@ -121,7 +121,14 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
setFilterState((prev) =>
prev.departure || prev.arrival
? prev
: { ...prev, departure: cityCode },
: {
...prev,
departure: cityCode,
// TZ §4.1.1 ¶4: with geo consent, enable domestic + international
domestic: true,
international: true,
// connections stays OFF per R14
},
),
});