Fix flights map transfer toggle fallback

This commit is contained in:
2026-05-21 19:07:13 +03:00
parent 99562c2218
commit 19dbdc5127
4 changed files with 167 additions and 6 deletions
@@ -10,6 +10,7 @@ import { transformDictionaries } from "@/shared/dictionaries/index.js";
import type { IDictionaries, IRawDictionaries } from "@/shared/dictionaries/index.js";
import type {
FlightsMapCalendarParams,
IFlightsMapFilterState,
FlightsMapSearchParams,
} from "../types.js";
import {
@@ -383,6 +384,7 @@ describe("TZ §4.1.4 Table 7 breadcrumbs — Flight-Map pages (rows 1-3)", () =>
describe("FlightsMapStartPage — C.4 integration", () => {
beforeEach(() => {
lastMapCanvasProps = null;
lastMapFilterProps = null;
searchCalls.length = 0;
dictState.dictionaries = buildDictionaries({
regions: [],
@@ -513,6 +515,33 @@ describe("FlightsMapStartPage — C.4 integration", () => {
const last = [...searchCalls].reverse().find((p) => p?.departure && p?.arrival);
expect(last?.connections).toBe(1);
});
it("keeps transfer-only toggle off after the user disables auto-fallback", () => {
searchState.routes = [];
render(<FlightsMapStartPage />);
act(() => {
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
});
act(() => {
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
});
const autoFallbackValue = lastMapFilterProps!["value"] as IFlightsMapFilterState;
expect(autoFallbackValue.connections).toBe(true);
act(() => {
(lastMapFilterProps!["onChange"] as (state: IFlightsMapFilterState) => void)({
...autoFallbackValue,
connections: false,
});
});
const userValue = lastMapFilterProps!["value"] as IFlightsMapFilterState;
expect(userValue.connections).toBe(false);
const last = [...searchCalls].reverse().find((p) => p?.departure && p?.arrival);
expect(last?.connections).toBe(0);
});
});
// ---------------------------------------------------------------------------
@@ -161,6 +161,8 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
const [effectiveConnections, setEffectiveConnections] = useState<0 | 1>(
filterState.connections ? 1 : 0,
);
const [connectionsFallbackSuppressed, setConnectionsFallbackSuppressed] =
useState(false);
const persistFilterState = useCallback((newState: IFlightsMapFilterState) => {
setFilterState(newState);
@@ -225,6 +227,7 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
useEffect(() => {
if (loading || error) return;
if (effectiveConnections !== 0) return;
if (connectionsFallbackSuppressed) return;
if (!filterState.departure || !filterState.arrival) return;
if (routes.length > 0) return;
setEffectiveConnections(1);
@@ -232,6 +235,7 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
loading,
error,
effectiveConnections,
connectionsFallbackSuppressed,
filterState.departure,
filterState.arrival,
routes,
@@ -239,14 +243,43 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
// Reflect fallback in the UI toggle once.
useEffect(() => {
if (filterState.arrival && effectiveConnections === 1 && !filterState.connections) {
if (
filterState.arrival &&
effectiveConnections === 1 &&
!filterState.connections &&
!connectionsFallbackSuppressed
) {
setFilterState((prev) => ({ ...prev, connections: true }));
}
}, [effectiveConnections, filterState.arrival, filterState.connections]);
}, [
effectiveConnections,
filterState.arrival,
filterState.connections,
connectionsFallbackSuppressed,
]);
const handleFilterChange = useCallback((newState: IFlightsMapFilterState) => {
const sameRoute =
newState.departure === filterState.departure &&
newState.arrival === filterState.arrival;
const turnedConnectionsOff =
sameRoute && filterState.connections && !newState.connections;
setConnectionsFallbackSuppressed(
turnedConnectionsOff
? true
: sameRoute && !newState.connections
? connectionsFallbackSuppressed
: false,
);
persistFilterState(newState);
}, [persistFilterState]);
}, [
connectionsFallbackSuppressed,
filterState.arrival,
filterState.connections,
filterState.departure,
persistFilterState,
]);
const handleMarkerClick = useCallback(
(markerId: string) => {
+90 -1
View File
@@ -1,11 +1,73 @@
import { test, expect } from "./fixtures/console-gate";
import {
routeAppSettingsFixture,
routeDictionaryFixtures,
} from "./helpers/api-fixtures";
async function routeFlightsMapTransferOnlyFixtures(
page: import("@playwright/test").Page,
): Promise<void> {
await routeAppSettingsFixture(page);
await routeDictionaryFixtures(page);
await page.route("**/api/flights/1/*/destinations?**", async (route) => {
const url = new URL(route.request().url());
const departure = url.searchParams.get("departure");
const arrival = url.searchParams.get("arrival");
const connections = url.searchParams.get("connections");
const hasTransferRoute =
departure === "LED" && arrival === "MLE" && connections === "1";
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
routes: hasTransferRoute
? [{ route: ["LED", "SVO", "MLE"], isDirect: false }]
: [],
},
}),
});
});
await page.route("**/api/flights/v1/*/days/**/flights-map/", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ days: "1".repeat(200) }),
}),
);
await page.route("**/map/api/tile/**", (route) =>
route.fulfill({
status: 200,
contentType: "image/gif",
body: Buffer.from("R0lGODlhAQABAAAAACw=", "base64"),
}),
);
}
async function selectCity(
page: import("@playwright/test").Page,
inputTestId: string,
query: string,
optionTestId: string,
): Promise<void> {
const input = page.getByTestId(inputTestId).locator("input");
await input.click();
await input.fill(query);
await expect(page.getByTestId(optionTestId)).toBeVisible({ timeout: 10000 });
await page.getByTestId(optionTestId).click();
}
test.describe("Flights Map", () => {
test("/ru/flights-map renders or shows feature-flag disabled message", async ({
page,
consoleMessages,
}) => {
await page.goto("/ru/flights-map");
await page.goto("/ru-ru/flights-map");
await page.waitForLoadState("domcontentloaded");
// Either the map page renders or the feature-flag-disabled fallback shows
@@ -14,4 +76,31 @@ test.describe("Flights Map", () => {
await expect(mapStart.or(mapDisabled)).toBeVisible({ timeout: 10000 });
});
test("transfer-only checkbox can be switched off after auto-fallback", async ({
page,
consoleMessages,
}) => {
await routeFlightsMapTransferOnlyFixtures(page);
await page.goto("/ru-ru/flights-map");
await expect(page.getByTestId("flights-map-start")).toBeVisible();
await selectCity(page, "fm-departure-input", "Санкт", "city-suggestion-LED");
await selectCity(page, "fm-arrival-input", "Мале", "city-suggestion-MLE");
const transferToggle = page.getByTestId("fm-connections-toggle");
const transferLabel = page.getByText("Показать только рейсы с пересадкой", {
exact: true,
});
await expect(transferToggle).toBeEnabled();
await expect(transferToggle).toBeChecked({ timeout: 10000 });
await transferLabel.click();
await expect(transferToggle).not.toBeChecked();
await page.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
await expect(transferToggle).not.toBeChecked();
expect(consoleMessages).toEqual([]);
});
});
+12 -2
View File
@@ -1,7 +1,14 @@
import { test, expect } from "./fixtures/console-gate";
import {
routeDictionaryFixtures,
routeOnlineboardRouteFixtures,
} from "./helpers/api-fixtures";
test.describe("Smoke tests", () => {
test("root / redirects to /ru/onlineboard", async ({ page, consoleMessages }) => {
await routeDictionaryFixtures(page);
await routeOnlineboardRouteFixtures(page);
await page.goto("/");
await page.waitForLoadState("domcontentloaded");
@@ -9,8 +16,8 @@ test.describe("Smoke tests", () => {
// Depending on SSR/CSR mode, this may be a server 302 or a client-side
// navigation. Wait up to 15s for either outcome.
try {
await page.waitForURL("**/ru/onlineboard", { timeout: 15000 });
expect(page.url()).toContain("/ru/onlineboard");
await page.waitForURL(/\/ru(?:-ru)?\/onlineboard$/, { timeout: 15000 });
expect(page.url()).toMatch(/\/ru(?:-ru)?\/onlineboard$/);
} catch {
// If the redirect doesn't fire (e.g. loader not invoked in dev SSR mode),
// verify the page at least rendered the online board content.
@@ -54,6 +61,9 @@ test.describe("Smoke tests", () => {
page,
consoleMessages,
}) => {
await routeDictionaryFixtures(page);
await routeOnlineboardRouteFixtures(page);
await page.goto("/ru-en/onlineboard?_preferredLanguage=en&_preferredLocale=ruh");
await page.waitForLoadState("domcontentloaded");