diff --git a/src/features/schedule/components/ScheduleSearchPage.tsx b/src/features/schedule/components/ScheduleSearchPage.tsx index 1dc95c6e..faae9762 100644 --- a/src/features/schedule/components/ScheduleSearchPage.tsx +++ b/src/features/schedule/components/ScheduleSearchPage.tsx @@ -138,13 +138,20 @@ export const ScheduleSearchPage: FC = ({ params }) => { childIds && childIds.length > 1 ? childIds : [flight.flightId]; - const buildSeg = (id: typeof flight.flightId): string => - buildFlightUrlParams({ + const buildSeg = ( + id: typeof flight.flightId, + leg?: typeof allLegs[number], + ): string => { + const localDate = leg?.departure.times.scheduledDeparture.local + ?.slice(0, 10) + .replace(/-/g, ""); + return buildFlightUrlParams({ carrier: id.carrier, flightNumber: id.flightNumber, ...(id.suffix ? { suffix: id.suffix } : {}), - date: id.date, + date: localDate || id.date, }); + }; // Interleave airport codes between flight segments so the URL // round-trips through `parseFlightSegments` (3-char codes are // skipped) AND keeps Angular-compatible structure for SEO. @@ -152,7 +159,7 @@ export const ScheduleSearchPage: FC = ({ params }) => { flightIds.forEach((id, i) => { const leg = allLegs[i] ?? allLegs[allLegs.length - 1]; if (i === 0 && leg) parts.push(leg.departure.scheduled.airportCode); - parts.push(buildSeg(id)); + parts.push(buildSeg(id, leg)); if (leg) parts.push(leg.arrival.scheduled.airportCode); }); const segment = parts.join("/"); diff --git a/tests/e2e/schedule-su0634-aircraft-link.spec.ts b/tests/e2e/schedule-su0634-aircraft-link.spec.ts new file mode 100644 index 00000000..2239728f --- /dev/null +++ b/tests/e2e/schedule-su0634-aircraft-link.spec.ts @@ -0,0 +1,173 @@ +import { test, expect } from "./fixtures/console-gate"; +import { + routeAppSettingsFixture, + routeDictionaryFixtures, +} from "./helpers/api-fixtures"; + +// TIRREDESIGN-28: SU0634 KJA -> HKT departs after midnight local time while +// the backend flightId.date remains on the previous service day. Angular builds +// the schedule details URL from the leg's local scheduled departure date, so +// the details request receives 2026-05-19 and the aircraft link is rendered. + +const ROUTE_URL = "/ru-ru/schedule/route/KJA-HKT-20260519-20260525-C0"; +const PLANE_PARK_URL = "http://www.aeroflot.ru/cms/ru/flight/plane_park"; + +const su0634Search = [ + { + routeType: "Direct", + operatingBy: { scheduled: "SU", operators: [] }, + status: "Scheduled", + id: "su0634-kja-hkt", + flightId: { + carrier: "SU", + flightNumber: "0634", + suffix: "", + date: "2026-05-18", + dateLT: "2026-05-19", + }, + flyingTime: "08:10:00", + leg: { + departure: { + scheduled: { + city: "Красноярск", + airport: "Емельяново", + countryCode: "RU", + cityCode: "KJA", + airportCode: "KJA", + }, + terminal: "1", + times: { + scheduledDeparture: { + utc: "2026-05-18T22:05:00Z", + local: "2026-05-19T05:05:00+07:00", + dayChange: { value: 0, title: "" }, + localTime: "05:05", + tzOffset: 420, + }, + }, + }, + arrival: { + scheduled: { + city: "Пхукет", + airport: "Пхукет", + countryCode: "TH", + cityCode: "HKT", + airportCode: "HKT", + }, + terminal: "I", + times: { + scheduledArrival: { + utc: "2026-05-19T06:15:00Z", + local: "2026-05-19T13:15:00+07:00", + dayChange: { value: 0, title: "" }, + localTime: "13:15", + tzOffset: 420, + }, + }, + }, + flags: { + checkinAvailable: false, + purchaseAvailable: true, + statusAvailable: false, + routeChanged: false, + returnToAirport: false, + }, + updated: "2026-05-15T03:29:55Z", + status: "Scheduled", + operatingBy: { scheduled: "SU", operators: [] }, + transition: {}, + daysOfWeek: { current: "2", flight: "25" }, + flyingTime: "08:10:00", + equipment: { + meal: [{ type: "Business" }, { type: "Economy" }, { type: "Comfort" }], + aircraft: { + scheduled: { type: "333", title: "Airbus A330-300" }, + actualType: { type: "333", title: "Airbus A330-300" }, + actual: { + type: "333", + title: "Airbus A330-300", + registration: "73786", + name: "В. Брумель", + }, + }, + }, + trafficRestrictions: [""], + }, + }, +]; + +const su0634Details = { + data: { + routes: su0634Search, + partners: [], + daysOfFlight: ["20260515", "20260519", "20260522", "20260526"], + }, +}; + +test("TIRREDESIGN-28: SU0634 schedule details uses local date and opens plane park", async ({ + page, + context, + consoleMessages, +}) => { + await routeDictionaryFixtures(page); + await routeAppSettingsFixture(page); + await page.route("**/api/flights/v1/*/days/**/schedule/", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ days: "1111111" }), + }); + }); + await page.route("**/api/flights/1/*/schedule?**", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(su0634Search), + }); + }); + await page.route("**/api/flights/v1.1/*/schedule/details?**", async (route) => { + const url = new URL(route.request().url()); + const date = url.searchParams.get("dates[0]"); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify( + date === "2026-05-19T00:00:00" + ? su0634Details + : { data: { routes: [], partners: [], daysOfFlight: [] } }, + ), + }); + }); + await context.route(PLANE_PARK_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: "text/html", + body: "Plane park", + }); + }); + + await page.goto(ROUTE_URL); + + const card = page.locator(".flight-card--clickable").first(); + await expect(card).toBeVisible({ timeout: 30000 }); + await card.click(); + + const detailsBtn = page.locator('[data-testid="flight-details-button"]'); + await expect(detailsBtn).toBeVisible({ timeout: 10000 }); + await detailsBtn.click(); + + await expect(page).toHaveURL(/\/ru-ru\/schedule\/KJA\/SU0634-20260519\/HKT/); + + const details = page.locator('[data-testid="schedule-leg-details"]'); + await expect(details).toBeVisible({ timeout: 15000 }); + const link = details.locator("a.schedule-leg-details__link"); + await expect(link).toHaveText("Airbus A330-300"); + await expect(link).toHaveAttribute("href", PLANE_PARK_URL); + await expect(link).toHaveAttribute("target", "_blank"); + + const popupPromise = page.waitForEvent("popup"); + await link.click(); + const popup = await popupPromise; + await expect(popup).toHaveURL(PLANE_PARK_URL); + await popup.close(); +});