This commit is contained in:
@@ -40,12 +40,14 @@ interface MockMap {
|
||||
_zoom: number;
|
||||
_eventHandlers: Record<string, Array<() => void>>;
|
||||
_addedLayers: Set<MockLayerGroup>;
|
||||
_container: HTMLElement;
|
||||
on: ReturnType<typeof vi.fn>;
|
||||
remove: ReturnType<typeof vi.fn>;
|
||||
addLayer: ReturnType<typeof vi.fn>;
|
||||
removeLayer: ReturnType<typeof vi.fn>;
|
||||
hasLayer: ReturnType<typeof vi.fn>;
|
||||
fitBounds: ReturnType<typeof vi.fn>;
|
||||
getContainer: ReturnType<typeof vi.fn>;
|
||||
getZoom: ReturnType<typeof vi.fn>;
|
||||
setZoom: (z: number) => void;
|
||||
fireZoomend: () => void;
|
||||
@@ -139,10 +141,12 @@ vi.mock("leaflet", () => {
|
||||
}
|
||||
|
||||
function mapFn(_container: unknown, _opts: unknown) {
|
||||
const container = document.createElement("div");
|
||||
const m: MockMap = {
|
||||
_zoom: 5,
|
||||
_eventHandlers: {},
|
||||
_addedLayers: new Set<MockLayerGroup>(),
|
||||
_container: container,
|
||||
on: vi.fn((evt: string, fn: () => void) => {
|
||||
m._eventHandlers[evt] ??= [];
|
||||
m._eventHandlers[evt]!.push(fn);
|
||||
@@ -167,6 +171,7 @@ vi.mock("leaflet", () => {
|
||||
return owning ? m._addedLayers.has(owning) : false;
|
||||
}),
|
||||
fitBounds: vi.fn(() => m),
|
||||
getContainer: vi.fn(() => container),
|
||||
getZoom: vi.fn(() => m._zoom),
|
||||
setZoom: (z: number) => {
|
||||
m._zoom = z;
|
||||
@@ -359,6 +364,27 @@ describe("MapCanvas — legacy (flat) path", () => {
|
||||
expect(L.DomEvent.stop).toHaveBeenCalled();
|
||||
expect(onMarkerClick).toHaveBeenCalledWith("MOW");
|
||||
});
|
||||
|
||||
it("selects marker icon clicks at the map container capture layer", () => {
|
||||
const onMarkerClick = vi.fn();
|
||||
renderCanvas(
|
||||
[{ id: "MOW", lat: 1, lng: 2, style: "blue-small", label: "Москва", tooltipPermanent: true }],
|
||||
{ onMarkerClick },
|
||||
);
|
||||
|
||||
const map = createdMaps[0];
|
||||
if (!map) {
|
||||
throw new Error("expected map to be created");
|
||||
}
|
||||
const markerIcon = document.createElement("img");
|
||||
markerIcon.className = "leaflet-marker-icon";
|
||||
markerIcon.setAttribute("title", "MOW");
|
||||
map._container.append(markerIcon);
|
||||
|
||||
markerIcon.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||
|
||||
expect(onMarkerClick).toHaveBeenCalledWith("MOW");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MapCanvas — categorized: visibility predicate", () => {
|
||||
|
||||
@@ -45,6 +45,16 @@ function getIcon(style: MarkerStyle): L.Icon {
|
||||
return L.icon(opts);
|
||||
}
|
||||
|
||||
function findMarkerIdFromEventTarget(target: EventTarget | null): string | undefined {
|
||||
if (!(target instanceof Element)) return undefined;
|
||||
|
||||
const tooltip = target.closest<HTMLElement>(".city-label[data-marker-id]");
|
||||
if (tooltip?.dataset.markerId) return tooltip.dataset.markerId;
|
||||
|
||||
const marker = target.closest<HTMLElement>(".leaflet-marker-icon[title]");
|
||||
return marker?.getAttribute("title") ?? undefined;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Polyline styles
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -325,6 +335,24 @@ export const MapCanvas: FC<MapCanvasProps> = ({
|
||||
minZoom: minZoomRef.current,
|
||||
}).addTo(map);
|
||||
|
||||
const container = map.getContainer();
|
||||
const stopCityPointer = (event: Event) => {
|
||||
const markerId = findMarkerIdFromEventTarget(event.target);
|
||||
if (!markerId || !markerIndexRef.current.has(markerId)) return;
|
||||
event.stopPropagation();
|
||||
};
|
||||
const selectCityFromDom = (event: Event) => {
|
||||
const markerId = findMarkerIdFromEventTarget(event.target);
|
||||
if (!markerId || !markerIndexRef.current.has(markerId)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onMarkerClickRef.current?.(markerId);
|
||||
};
|
||||
container.addEventListener("pointerdown", stopCityPointer, true);
|
||||
container.addEventListener("mousedown", stopCityPointer, true);
|
||||
container.addEventListener("touchstart", stopCityPointer, true);
|
||||
container.addEventListener("click", selectCityFromDom, true);
|
||||
|
||||
markersLayerRef.current = L.layerGroup().addTo(map);
|
||||
polylinesLayerRef.current = L.layerGroup().addTo(map);
|
||||
popupsLayerRef.current = L.layerGroup().addTo(map);
|
||||
@@ -356,6 +384,10 @@ export const MapCanvas: FC<MapCanvasProps> = ({
|
||||
mapRef.current = map;
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("pointerdown", stopCityPointer, true);
|
||||
container.removeEventListener("mousedown", stopCityPointer, true);
|
||||
container.removeEventListener("touchstart", stopCityPointer, true);
|
||||
container.removeEventListener("click", selectCityFromDom, true);
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
markersLayerRef.current = null;
|
||||
@@ -407,9 +439,35 @@ export const MapCanvas: FC<MapCanvasProps> = ({
|
||||
});
|
||||
|
||||
const tooltip = marker.getTooltip();
|
||||
const attachTooltipDomHandlers = () => {
|
||||
const element = tooltip?.getElement();
|
||||
if (!element || element.dataset.markerClickBound === m.id) return;
|
||||
|
||||
element.dataset.markerClickBound = m.id;
|
||||
element.dataset.markerId = m.id;
|
||||
|
||||
const stopTooltipPointerEvent = (event: Event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
const selectFromTooltip = (event: Event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onMarkerClickRef.current?.(m.id);
|
||||
};
|
||||
|
||||
element.addEventListener("pointerdown", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("pointerup", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("mousedown", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("mouseup", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("touchstart", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("touchend", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("dblclick", stopTooltipPointerEvent, true);
|
||||
element.addEventListener("click", selectFromTooltip, true);
|
||||
};
|
||||
const stopTooltipEvent = (event: L.LeafletMouseEvent) => {
|
||||
L.DomEvent.stop(event);
|
||||
};
|
||||
marker.on("tooltipopen", attachTooltipDomHandlers);
|
||||
tooltip?.on("mousedown", stopTooltipEvent);
|
||||
tooltip?.on("mouseup", stopTooltipEvent);
|
||||
tooltip?.on("dblclick", stopTooltipEvent);
|
||||
|
||||
@@ -102,6 +102,56 @@ async function routeFlightsMapDirectRouteFixtures(
|
||||
return destinationRequests;
|
||||
}
|
||||
|
||||
async function routeFlightsMapMoscowSpiderFixtures(
|
||||
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");
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
routes:
|
||||
departure === "MOW" && !arrival
|
||||
? [
|
||||
{ route: ["SVO", "LED"], isDirect: true },
|
||||
{ route: ["SVO", "MLE"], isDirect: true },
|
||||
]
|
||||
: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
page: import("@playwright/test").Page,
|
||||
inputTestId: string,
|
||||
@@ -157,6 +207,70 @@ test.describe("Flights Map", () => {
|
||||
expect(consoleMessages).toEqual([]);
|
||||
});
|
||||
|
||||
test("first click on a city label selects the city and draws spider routes", async ({
|
||||
page,
|
||||
consoleMessages,
|
||||
}) => {
|
||||
const destinationRequests = await routeFlightsMapMoscowSpiderFixtures(page);
|
||||
|
||||
await page.goto("/ru-ru/flights-map");
|
||||
await expect(page.getByTestId("flights-map-start")).toBeVisible();
|
||||
|
||||
const moscowLabel = page.locator(".leaflet-tooltip.city-label", {
|
||||
hasText: "Москва",
|
||||
});
|
||||
await expect(moscowLabel).toHaveCount(1, { timeout: 10000 });
|
||||
|
||||
await moscowLabel.click();
|
||||
|
||||
await expect(page.getByTestId("fm-departure-input").locator("input")).toHaveValue(
|
||||
"Москва",
|
||||
);
|
||||
await expect(page.getByTestId("fm-arrival-input").locator("input")).toHaveValue("");
|
||||
await expect
|
||||
.poll(() =>
|
||||
destinationRequests.some(
|
||||
(url) =>
|
||||
url.includes("departure=MOW") &&
|
||||
!url.includes("arrival="),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(page.locator("path.leaflet-interactive")).not.toHaveCount(0);
|
||||
|
||||
expect(consoleMessages).toEqual([]);
|
||||
});
|
||||
|
||||
test("first click on a city marker selects the city", async ({
|
||||
page,
|
||||
consoleMessages,
|
||||
}) => {
|
||||
const destinationRequests = await routeFlightsMapMoscowSpiderFixtures(page);
|
||||
|
||||
await page.goto("/ru-ru/flights-map");
|
||||
await expect(page.getByTestId("flights-map-start")).toBeVisible();
|
||||
|
||||
const moscowMarker = page.locator('img.leaflet-marker-icon[title="MOW"]');
|
||||
await expect(moscowMarker).toHaveCount(1, { timeout: 10000 });
|
||||
|
||||
await moscowMarker.click();
|
||||
|
||||
await expect(page.getByTestId("fm-departure-input").locator("input")).toHaveValue(
|
||||
"Москва",
|
||||
);
|
||||
await expect
|
||||
.poll(() =>
|
||||
destinationRequests.some(
|
||||
(url) =>
|
||||
url.includes("departure=MOW") &&
|
||||
!url.includes("arrival="),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
expect(consoleMessages).toEqual([]);
|
||||
});
|
||||
|
||||
test("transfer-only checkbox stays off while a direct route search is still loading", async ({
|
||||
page,
|
||||
consoleMessages,
|
||||
|
||||
Reference in New Issue
Block a user