Fix flights map transfer fallback timing
This commit is contained in:
@@ -73,10 +73,12 @@ const searchState: {
|
|||||||
routes: Array<{ route: string[]; isDirect: boolean }>;
|
routes: Array<{ route: string[]; isDirect: boolean }>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
|
completedParams: FlightsMapSearchParams | null;
|
||||||
} = {
|
} = {
|
||||||
routes: [],
|
routes: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
completedParams: null,
|
||||||
};
|
};
|
||||||
const searchCalls: Array<FlightsMapSearchParams | null> = [];
|
const searchCalls: Array<FlightsMapSearchParams | null> = [];
|
||||||
vi.mock("../hooks/useFlightsMapSearch.js", () => ({
|
vi.mock("../hooks/useFlightsMapSearch.js", () => ({
|
||||||
@@ -317,6 +319,7 @@ describe("FlightsMapStartPage — polylines from search results (C.3)", () => {
|
|||||||
searchState.routes = [];
|
searchState.routes = [];
|
||||||
searchState.loading = false;
|
searchState.loading = false;
|
||||||
searchState.error = null;
|
searchState.error = null;
|
||||||
|
searchState.completedParams = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes an empty polylines array when no routes", () => {
|
it("passes an empty polylines array when no routes", () => {
|
||||||
@@ -510,9 +513,9 @@ describe("FlightsMapStartPage — C.4 integration", () => {
|
|||||||
expect(popups).toEqual([]);
|
expect(popups).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("auto-fallback: re-issues the search with connections=1 when direct routes come back empty", () => {
|
it("auto-fallback: re-issues the search with connections=1 when a completed direct search comes back empty", () => {
|
||||||
searchState.routes = [];
|
searchState.routes = [];
|
||||||
render(<FlightsMapStartPage />);
|
const { rerender } = render(<FlightsMapStartPage />);
|
||||||
act(() => {
|
act(() => {
|
||||||
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
||||||
});
|
});
|
||||||
@@ -520,13 +523,21 @@ describe("FlightsMapStartPage — C.4 integration", () => {
|
|||||||
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const direct = [...searchCalls]
|
||||||
|
.reverse()
|
||||||
|
.find((p) => p?.departure === "MOW" && p?.arrival === "LED" && p?.connections === 0);
|
||||||
|
searchState.completedParams = direct ?? null;
|
||||||
|
act(() => {
|
||||||
|
rerender(<FlightsMapStartPage />);
|
||||||
|
});
|
||||||
|
|
||||||
const withOne = searchCalls.filter((p) => p?.connections === 1);
|
const withOne = searchCalls.filter((p) => p?.connections === 1);
|
||||||
expect(withOne.length).toBeGreaterThanOrEqual(1);
|
expect(withOne.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mirror: last search call uses connections=1 after auto-fallback", () => {
|
it("mirror: last search call uses connections=1 after completed auto-fallback", () => {
|
||||||
searchState.routes = [];
|
searchState.routes = [];
|
||||||
render(<FlightsMapStartPage />);
|
const { rerender } = render(<FlightsMapStartPage />);
|
||||||
act(() => {
|
act(() => {
|
||||||
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
||||||
});
|
});
|
||||||
@@ -534,13 +545,21 @@ describe("FlightsMapStartPage — C.4 integration", () => {
|
|||||||
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const direct = [...searchCalls]
|
||||||
|
.reverse()
|
||||||
|
.find((p) => p?.departure === "MOW" && p?.arrival === "LED" && p?.connections === 0);
|
||||||
|
searchState.completedParams = direct ?? null;
|
||||||
|
act(() => {
|
||||||
|
rerender(<FlightsMapStartPage />);
|
||||||
|
});
|
||||||
|
|
||||||
const last = [...searchCalls].reverse().find((p) => p?.departure && p?.arrival);
|
const last = [...searchCalls].reverse().find((p) => p?.departure && p?.arrival);
|
||||||
expect(last?.connections).toBe(1);
|
expect(last?.connections).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps transfer-only toggle off after the user disables auto-fallback", () => {
|
it("keeps transfer-only toggle off after the user disables auto-fallback", () => {
|
||||||
searchState.routes = [];
|
searchState.routes = [];
|
||||||
render(<FlightsMapStartPage />);
|
const { rerender } = render(<FlightsMapStartPage />);
|
||||||
act(() => {
|
act(() => {
|
||||||
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
||||||
});
|
});
|
||||||
@@ -548,6 +567,14 @@ describe("FlightsMapStartPage — C.4 integration", () => {
|
|||||||
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const direct = [...searchCalls]
|
||||||
|
.reverse()
|
||||||
|
.find((p) => p?.departure === "MOW" && p?.arrival === "LED" && p?.connections === 0);
|
||||||
|
searchState.completedParams = direct ?? null;
|
||||||
|
act(() => {
|
||||||
|
rerender(<FlightsMapStartPage />);
|
||||||
|
});
|
||||||
|
|
||||||
const autoFallbackValue = lastMapFilterProps!["value"] as IFlightsMapFilterState;
|
const autoFallbackValue = lastMapFilterProps!["value"] as IFlightsMapFilterState;
|
||||||
expect(autoFallbackValue.connections).toBe(true);
|
expect(autoFallbackValue.connections).toBe(true);
|
||||||
|
|
||||||
@@ -564,6 +591,22 @@ describe("FlightsMapStartPage — C.4 integration", () => {
|
|||||||
const last = [...searchCalls].reverse().find((p) => p?.departure && p?.arrival);
|
const last = [...searchCalls].reverse().find((p) => p?.departure && p?.arrival);
|
||||||
expect(last?.connections).toBe(0);
|
expect(last?.connections).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not auto-fallback before the current direct search has completed", () => {
|
||||||
|
searchState.routes = [];
|
||||||
|
searchState.completedParams = null;
|
||||||
|
render(<FlightsMapStartPage />);
|
||||||
|
act(() => {
|
||||||
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("MOW");
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
(lastMapCanvasProps!["onMarkerClick"] as (id: string) => void)("LED");
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = lastMapFilterProps!["value"] as IFlightsMapFilterState;
|
||||||
|
expect(value.connections).toBe(false);
|
||||||
|
expect(searchCalls.some((p) => p?.connections === 1)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -220,12 +220,21 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
|||||||
searchDateFromYmd,
|
searchDateFromYmd,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { routes, loading, error } = useFlightsMapSearch(searchParams);
|
const { routes, loading, error, completedParams } =
|
||||||
|
useFlightsMapSearch(searchParams);
|
||||||
const { availableDays } = useFlightsMapCalendar(calendarParams);
|
const { availableDays } = useFlightsMapCalendar(calendarParams);
|
||||||
|
|
||||||
// Auto-fallback: empty result, route mode, connections=0 → retry with 1.
|
// Auto-fallback: empty result, route mode, connections=0 → retry with 1.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading || error) return;
|
if (loading || error) return;
|
||||||
|
if (!searchParams || searchParams.connections !== 0) return;
|
||||||
|
const directSearchCompleted =
|
||||||
|
completedParams?.departure === searchParams.departure &&
|
||||||
|
completedParams?.arrival === searchParams.arrival &&
|
||||||
|
completedParams?.dateFrom === searchParams.dateFrom &&
|
||||||
|
completedParams?.dateTo === searchParams.dateTo &&
|
||||||
|
completedParams?.connections === 0;
|
||||||
|
if (!directSearchCompleted) return;
|
||||||
if (effectiveConnections !== 0) return;
|
if (effectiveConnections !== 0) return;
|
||||||
if (connectionsFallbackSuppressed) return;
|
if (connectionsFallbackSuppressed) return;
|
||||||
if (!filterState.departure || !filterState.arrival) return;
|
if (!filterState.departure || !filterState.arrival) return;
|
||||||
@@ -236,9 +245,11 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
|||||||
error,
|
error,
|
||||||
effectiveConnections,
|
effectiveConnections,
|
||||||
connectionsFallbackSuppressed,
|
connectionsFallbackSuppressed,
|
||||||
|
completedParams,
|
||||||
filterState.departure,
|
filterState.departure,
|
||||||
filterState.arrival,
|
filterState.arrival,
|
||||||
routes,
|
routes,
|
||||||
|
searchParams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Reflect fallback in the UI toggle once.
|
// Reflect fallback in the UI toggle once.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface UseFlightsMapSearchResult {
|
|||||||
routes: IFlightRoute[];
|
routes: IFlightRoute[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: ApiError | null;
|
error: ApiError | null;
|
||||||
|
completedParams: FlightsMapSearchParams | null;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@ export function useFlightsMapSearch(
|
|||||||
const [routes, setRoutes] = useState<IFlightRoute[]>([]);
|
const [routes, setRoutes] = useState<IFlightRoute[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<ApiError | null>(null);
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [completedParams, setCompletedParams] =
|
||||||
|
useState<FlightsMapSearchParams | null>(null);
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
const paramsRef = useRef(params);
|
const paramsRef = useRef(params);
|
||||||
@@ -44,18 +47,21 @@ export function useFlightsMapSearch(
|
|||||||
if (!paramsRef.current) {
|
if (!paramsRef.current) {
|
||||||
setRoutes([]);
|
setRoutes([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setCompletedParams(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestParams = { ...paramsRef.current };
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setRoutes([]);
|
setRoutes([]);
|
||||||
|
|
||||||
searchDestinations(client, paramsRef.current)
|
searchDestinations(client, requestParams)
|
||||||
.then((response: IDestinationsResponse) => {
|
.then((response: IDestinationsResponse) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRoutes(response.data.routes);
|
setRoutes(response.data.routes);
|
||||||
|
setCompletedParams(requestParams);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -63,6 +69,7 @@ export function useFlightsMapSearch(
|
|||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError(err);
|
setError(err);
|
||||||
setRoutes([]);
|
setRoutes([]);
|
||||||
|
setCompletedParams(requestParams);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -80,5 +87,5 @@ export function useFlightsMapSearch(
|
|||||||
refreshKey,
|
refreshKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { routes, loading, error, refresh };
|
return { routes, loading, error, completedParams, refresh };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,59 @@ async function routeFlightsMapTransferOnlyFixtures(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function routeFlightsMapDirectRouteFixtures(
|
||||||
|
page: import("@playwright/test").Page,
|
||||||
|
): Promise<string[]> {
|
||||||
|
await routeAppSettingsFixture(page);
|
||||||
|
await routeDictionaryFixtures(page);
|
||||||
|
|
||||||
|
const destinationRequests: string[] = [];
|
||||||
|
|
||||||
|
await page.route("**/api/flights/1/*/destinations?**", async (route) => {
|
||||||
|
const url = new URL(route.request().url());
|
||||||
|
destinationRequests.push(url.toString());
|
||||||
|
const departure = url.searchParams.get("departure");
|
||||||
|
const arrival = url.searchParams.get("arrival");
|
||||||
|
const connections = url.searchParams.get("connections");
|
||||||
|
|
||||||
|
const isSelectedRoute = departure === "LED" && arrival === "MLE";
|
||||||
|
const routes =
|
||||||
|
isSelectedRoute && connections === "0"
|
||||||
|
? [{ route: ["LED", "MLE"], isDirect: true }]
|
||||||
|
: isSelectedRoute && connections === "1"
|
||||||
|
? [{ route: ["LED", "SVO", "MLE"], isDirect: false }]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (isSelectedRoute && connections === "0") {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ data: { routes } }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return destinationRequests;
|
||||||
|
}
|
||||||
|
|
||||||
async function selectCity(
|
async function selectCity(
|
||||||
page: import("@playwright/test").Page,
|
page: import("@playwright/test").Page,
|
||||||
inputTestId: string,
|
inputTestId: string,
|
||||||
@@ -103,4 +156,44 @@ test.describe("Flights Map", () => {
|
|||||||
|
|
||||||
expect(consoleMessages).toEqual([]);
|
expect(consoleMessages).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("transfer-only checkbox stays off while a direct route search is still loading", async ({
|
||||||
|
page,
|
||||||
|
consoleMessages,
|
||||||
|
}) => {
|
||||||
|
const destinationRequests = await routeFlightsMapDirectRouteFixtures(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");
|
||||||
|
await expect(transferToggle).toBeEnabled();
|
||||||
|
await expect(transferToggle).not.toBeChecked();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
destinationRequests.some(
|
||||||
|
(url) =>
|
||||||
|
url.includes("departure=LED") &&
|
||||||
|
url.includes("arrival=MLE") &&
|
||||||
|
url.includes("connections=0"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toBe(true);
|
||||||
|
await expect(transferToggle).not.toBeChecked();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
expect(
|
||||||
|
destinationRequests.some(
|
||||||
|
(url) =>
|
||||||
|
url.includes("departure=LED") &&
|
||||||
|
url.includes("arrival=MLE") &&
|
||||||
|
url.includes("connections=1"),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(consoleMessages).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user