Fix schedule aircraft link target
This commit is contained in:
@@ -473,7 +473,10 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
|
|||||||
<FlightCard flight={flight} direction="schedule" />
|
<FlightCard flight={flight} direction="schedule" />
|
||||||
<IFlyWarning flightNumber={flight.flightId.flightNumber} />
|
<IFlyWarning flightNumber={flight.flightId.flightNumber} />
|
||||||
{renderBody(flight)}
|
{renderBody(flight)}
|
||||||
<ScheduleLegDetails flight={flight as unknown as ISimpleFlight} />
|
<ScheduleLegDetails
|
||||||
|
flight={flight as unknown as ISimpleFlight}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Angular hides the weekly operating schedule on
|
{/* Angular hides the weekly operating schedule on
|
||||||
multi-leg chains; keep it on direct flights. */}
|
multi-leg chains; keep it on direct flights. */}
|
||||||
|
|||||||
@@ -42,6 +42,30 @@ function makeFlight(meal: MealType[] | undefined): ISimpleFlight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("ScheduleLegDetails Питание sub-icons", () => {
|
describe("ScheduleLegDetails Питание sub-icons", () => {
|
||||||
|
it("links the aircraft title to Angular's Aeroflot plane park URL", () => {
|
||||||
|
render(<ScheduleLegDetails flight={makeFlight([])} locale="ru-ru" />);
|
||||||
|
|
||||||
|
const link = screen.getByRole("link", { name: "Sukhoi SuperJet 100" });
|
||||||
|
|
||||||
|
expect(link.getAttribute("href")).toBe(
|
||||||
|
"http://www.aeroflot.ru/cms/ru/flight/plane_park",
|
||||||
|
);
|
||||||
|
expect(link.getAttribute("target")).toBe("_blank");
|
||||||
|
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the language prefix from locale for the plane park URL", () => {
|
||||||
|
render(<ScheduleLegDetails flight={makeFlight([])} locale="en-us" />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getByRole("link", { name: "Sukhoi SuperJet 100" })
|
||||||
|
.getAttribute("href"),
|
||||||
|
).toBe(
|
||||||
|
"http://www.aeroflot.ru/cms/en/flight/plane_park",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders no meal-class sub-icons when equipment.meal is empty", () => {
|
it("renders no meal-class sub-icons when equipment.meal is empty", () => {
|
||||||
render(<ScheduleLegDetails flight={makeFlight([])} />);
|
render(<ScheduleLegDetails flight={makeFlight([])} />);
|
||||||
expect(screen.queryByText("FOOD.ECONOMY")).toBeNull();
|
expect(screen.queryByText("FOOD.ECONOMY")).toBeNull();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* each flight leg. Mirrors Angular's `flight-details-wrapper` accordion
|
* each flight leg. Mirrors Angular's `flight-details-wrapper` accordion
|
||||||
* (schedule view): two rows when the data exists.
|
* (schedule view): two rows when the data exists.
|
||||||
*
|
*
|
||||||
* 1. Борт — aircraft type with a link to its Aeroflot info page.
|
* 1. Борт — aircraft type with a link to Aeroflot's aircraft park.
|
||||||
* 2. Питание на борту — meal-class sub-icons (Эконом / Комфорт /
|
* 2. Питание на борту — meal-class sub-icons (Эконом / Комфорт /
|
||||||
* Бизнес), each rendered ONLY when the API's
|
* Бизнес), each rendered ONLY when the API's
|
||||||
* `equipment.meal[]` array contains the matching `type`. Matches
|
* `equipment.meal[]` array contains the matching `type`. Matches
|
||||||
@@ -25,18 +25,18 @@ import "./ScheduleLegDetails.scss";
|
|||||||
|
|
||||||
export interface ScheduleLegDetailsProps {
|
export interface ScheduleLegDetailsProps {
|
||||||
flight: ISimpleFlight;
|
flight: ISimpleFlight;
|
||||||
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Slug used for the aircraft catalog deep-link on www.aeroflot.ru. */
|
function aircraftParkHref(locale: string | undefined): string {
|
||||||
function aircraftSlug(title: string | undefined | null): string | null {
|
const language = locale?.split("-")[0] || "ru";
|
||||||
if (!title) return null;
|
return `http://www.aeroflot.ru/cms/${language}/flight/plane_park`;
|
||||||
return title
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, "-")
|
|
||||||
.replace(/[^a-z0-9-]/g, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScheduleLegDetails: FC<ScheduleLegDetailsProps> = ({ flight }) => {
|
export const ScheduleLegDetails: FC<ScheduleLegDetailsProps> = ({
|
||||||
|
flight,
|
||||||
|
locale,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [expanded, setExpanded] = useState<boolean>(true);
|
const [expanded, setExpanded] = useState<boolean>(true);
|
||||||
|
|
||||||
@@ -47,13 +47,7 @@ export const ScheduleLegDetails: FC<ScheduleLegDetailsProps> = ({ flight }) => {
|
|||||||
leg.equipment?.aircraft?.actual?.title ??
|
leg.equipment?.aircraft?.actual?.title ??
|
||||||
leg.equipment?.aircraft?.scheduled?.title ??
|
leg.equipment?.aircraft?.scheduled?.title ??
|
||||||
"";
|
"";
|
||||||
const slug = aircraftSlug(aircraft);
|
const planeUrl = aircraftParkHref(locale);
|
||||||
// External Aeroflot page that describes the aircraft model. Angular
|
|
||||||
// uses a curated slug map; the simple kebab-case works for common
|
|
||||||
// models (e.g. sukhoi-superjet-100).
|
|
||||||
const planeUrl = slug
|
|
||||||
? `https://www.aeroflot.ru/ru-ru/about/aircrafts/${slug}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="schedule-leg-details" data-testid="schedule-leg-details">
|
<div className="schedule-leg-details" data-testid="schedule-leg-details">
|
||||||
@@ -117,18 +111,14 @@ export const ScheduleLegDetails: FC<ScheduleLegDetailsProps> = ({ flight }) => {
|
|||||||
<span className="schedule-leg-details__label">
|
<span className="schedule-leg-details__label">
|
||||||
{t("SHARED.PLANE")}
|
{t("SHARED.PLANE")}
|
||||||
</span>
|
</span>
|
||||||
{planeUrl ? (
|
<a
|
||||||
<a
|
className="schedule-leg-details__link"
|
||||||
className="schedule-leg-details__link"
|
href={planeUrl}
|
||||||
href={planeUrl}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
>
|
{aircraft}
|
||||||
{aircraft}
|
</a>
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="schedule-leg-details__value">{aircraft}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { test, expect } from "./fixtures/console-gate";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
// Schedule details uses the same Angular aircraft-link behavior as
|
||||||
|
// online-board: the model text under "Борт" opens the generic plane park page.
|
||||||
|
|
||||||
|
const FIXTURE_DIR = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"../fixtures/api",
|
||||||
|
);
|
||||||
|
const scheduleDetails = fs.readFileSync(
|
||||||
|
path.join(FIXTURE_DIR, "schedule-details.json"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const URL =
|
||||||
|
"/ru-ru/schedule/VVO/SU5752-20260518/KJA/SU6837-20260519/MJZ?request=schedule-route-VVO-MJZ-20260518-20260524";
|
||||||
|
|
||||||
|
test("Schedule details aircraft title opens Aeroflot plane park in a new tab", async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
consoleMessages,
|
||||||
|
}) => {
|
||||||
|
await page.route("**/api/flights/v1.1/ru/schedule/details?**", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: scheduleDetails,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.route("http://www.aeroflot.ru/cms/ru/flight/plane_park", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "text/html",
|
||||||
|
body: "<!doctype html><title>Plane park</title>",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(URL);
|
||||||
|
|
||||||
|
const details = page.locator(".schedule-leg-details");
|
||||||
|
await expect(details).toHaveCount(1, { timeout: 15000 });
|
||||||
|
await expect(details.getByText("Борт", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
const link = details.locator("a.schedule-leg-details__link");
|
||||||
|
await expect(link).toHaveText("Sukhoi SuperJet 100");
|
||||||
|
await expect(link).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"http://www.aeroflot.ru/cms/ru/flight/plane_park",
|
||||||
|
);
|
||||||
|
await expect(link).toHaveAttribute("target", "_blank");
|
||||||
|
|
||||||
|
const popupPromise = page.waitForEvent("popup");
|
||||||
|
await link.click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
await expect(popup).toHaveURL("http://www.aeroflot.ru/cms/ru/flight/plane_park");
|
||||||
|
await popup.close();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user