e20686b11f
Each route page now renders <SeoHead> with title, description, canonical, hreflang, OG, and Twitter card from the SEO builders. Search pages render <JsonLdRenderer> with ItemList of Flight schemas. Details page renders Flight JSON-LD. Barrel exports updated with 2F SEO and JSON-LD functions.
294 lines
8.4 KiB
TypeScript
294 lines
8.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildOnlineBoardStartSeo,
|
|
buildFlightSearchSeo,
|
|
buildDepartureSearchSeo,
|
|
buildArrivalSearchSeo,
|
|
buildRouteSearchSeo,
|
|
buildFlightDetailsSeo,
|
|
} from "./seo.js";
|
|
import type { ISimpleFlight } from "./types.js";
|
|
|
|
/** Stub t() that returns the key + interpolation vars for assertion. */
|
|
function stubT(key: string, opts?: Record<string, unknown>): string {
|
|
if (opts && Object.keys(opts).length > 0) {
|
|
const vars = Object.entries(opts)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(",");
|
|
return `${key}|${vars}`;
|
|
}
|
|
return key;
|
|
}
|
|
|
|
const CANONICAL = "https://www.aeroflot.ru";
|
|
|
|
describe("buildOnlineBoardStartSeo", () => {
|
|
it("uses MAIN translation keys for title and description", () => {
|
|
const result = buildOnlineBoardStartSeo(stubT, "ru", CANONICAL);
|
|
|
|
expect(result.title).toBe("SEO.BOARD.MAIN.TITLE");
|
|
expect(result.description).toBe("SEO.BOARD.MAIN.DESCRIPTION");
|
|
});
|
|
|
|
it("sets canonical to /{locale}/onlineboard", () => {
|
|
const result = buildOnlineBoardStartSeo(stubT, "en", CANONICAL);
|
|
|
|
expect(result.canonical).toBe("https://www.aeroflot.ru/en/onlineboard");
|
|
});
|
|
|
|
it("includes hreflang with 10 entries (9 langs + x-default)", () => {
|
|
const result = buildOnlineBoardStartSeo(stubT, "ru", CANONICAL);
|
|
|
|
expect(result.hreflang).toHaveLength(10);
|
|
});
|
|
|
|
it("sets og tags", () => {
|
|
const result = buildOnlineBoardStartSeo(stubT, "ru", CANONICAL);
|
|
|
|
expect(result.og.title).toBe(result.title);
|
|
expect(result.og.description).toBe(result.description);
|
|
expect(result.og.url).toBe(result.canonical);
|
|
expect(result.og.type).toBe("website");
|
|
expect(result.og.locale).toBe("ru");
|
|
expect(result.og.siteName).toBe("Aeroflot");
|
|
expect(result.og.image).toContain("aeroflot");
|
|
});
|
|
|
|
it("sets twitter card", () => {
|
|
const result = buildOnlineBoardStartSeo(stubT, "ru", CANONICAL);
|
|
|
|
expect(result.twitter).toBeDefined();
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- test assertion guards above
|
|
expect(result.twitter!.card).toBe("summary");
|
|
});
|
|
});
|
|
|
|
describe("buildFlightSearchSeo", () => {
|
|
const params = {
|
|
type: "flight" as const,
|
|
carrier: "SU",
|
|
flightNumber: "0100",
|
|
date: "20250115",
|
|
};
|
|
|
|
it("uses FLIGHT-SEARCH translation keys with interpolation", () => {
|
|
const result = buildFlightSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.title).toContain("SEO.BOARD.FLIGHT-SEARCH.TITLE");
|
|
expect(result.title).toContain("flightNumber=SU 0100");
|
|
expect(result.description).toContain("SEO.BOARD.FLIGHT-SEARCH.DESCRIPTION");
|
|
});
|
|
|
|
it("sets canonical to the flight search URL", () => {
|
|
const result = buildFlightSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.canonical).toBe(
|
|
"https://www.aeroflot.ru/ru/onlineboard/flight/SU0100-20250115",
|
|
);
|
|
});
|
|
|
|
it("includes hreflang entries", () => {
|
|
const result = buildFlightSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.hreflang).toHaveLength(10);
|
|
});
|
|
});
|
|
|
|
describe("buildDepartureSearchSeo", () => {
|
|
const params = {
|
|
type: "departure" as const,
|
|
station: "SVO",
|
|
date: "20250115",
|
|
};
|
|
|
|
it("uses DEPARTURE-SEARCH translation keys with city name", () => {
|
|
const result = buildDepartureSearchSeo(
|
|
stubT,
|
|
params,
|
|
"ru",
|
|
CANONICAL,
|
|
{ departure: "Moscow" },
|
|
);
|
|
|
|
expect(result.title).toContain("SEO.BOARD.DEPARTURE-SEARCH.TITLE");
|
|
expect(result.title).toContain("departureCity=Moscow");
|
|
});
|
|
|
|
it("falls back to station code when no city name provided", () => {
|
|
const result = buildDepartureSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.title).toContain("departureCity=SVO");
|
|
});
|
|
|
|
it("sets canonical to the departure search URL", () => {
|
|
const result = buildDepartureSearchSeo(stubT, params, "en", CANONICAL);
|
|
|
|
expect(result.canonical).toBe(
|
|
"https://www.aeroflot.ru/en/onlineboard/departure/SVO-20250115",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("buildArrivalSearchSeo", () => {
|
|
const params = {
|
|
type: "arrival" as const,
|
|
station: "JFK",
|
|
date: "20250115",
|
|
};
|
|
|
|
it("uses ARRIVAL-SEARCH translation keys with city name", () => {
|
|
const result = buildArrivalSearchSeo(
|
|
stubT,
|
|
params,
|
|
"ru",
|
|
CANONICAL,
|
|
{ arrival: "New York" },
|
|
);
|
|
|
|
expect(result.title).toContain("SEO.BOARD.ARRIVAL-SEARCH.TITLE");
|
|
expect(result.title).toContain("arrivalCity=New York");
|
|
});
|
|
|
|
it("falls back to station code when no city name provided", () => {
|
|
const result = buildArrivalSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.title).toContain("arrivalCity=JFK");
|
|
});
|
|
|
|
it("sets canonical to the arrival search URL", () => {
|
|
const result = buildArrivalSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.canonical).toBe(
|
|
"https://www.aeroflot.ru/ru/onlineboard/arrival/JFK-20250115",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("buildRouteSearchSeo", () => {
|
|
const params = {
|
|
type: "route" as const,
|
|
departure: "SVO",
|
|
arrival: "JFK",
|
|
date: "20250115",
|
|
};
|
|
|
|
it("uses ROUTE-SEARCH translation keys with both city names", () => {
|
|
const result = buildRouteSearchSeo(
|
|
stubT,
|
|
params,
|
|
"ru",
|
|
CANONICAL,
|
|
{ departure: "Moscow", arrival: "New York" },
|
|
);
|
|
|
|
expect(result.title).toContain("SEO.BOARD.ROUTE-SEARCH.TITLE");
|
|
expect(result.title).toContain("departureCity=Moscow");
|
|
expect(result.title).toContain("arrivalCity=New York");
|
|
});
|
|
|
|
it("falls back to IATA codes when no city names provided", () => {
|
|
const result = buildRouteSearchSeo(stubT, params, "ru", CANONICAL);
|
|
|
|
expect(result.title).toContain("departureCity=SVO");
|
|
expect(result.title).toContain("arrivalCity=JFK");
|
|
});
|
|
|
|
it("sets canonical to the route search URL", () => {
|
|
const result = buildRouteSearchSeo(stubT, params, "en", CANONICAL);
|
|
|
|
expect(result.canonical).toBe(
|
|
"https://www.aeroflot.ru/en/onlineboard/route/SVO-JFK-20250115",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("buildFlightDetailsSeo", () => {
|
|
const flight: ISimpleFlight = {
|
|
id: "SU100-20250115",
|
|
routeType: "Direct",
|
|
flightId: {
|
|
carrier: "SU",
|
|
flightNumber: "0100",
|
|
suffix: "",
|
|
date: "20250115",
|
|
},
|
|
flyingTime: "10h 30m",
|
|
operatingBy: {},
|
|
status: "Scheduled",
|
|
leg: {
|
|
index: 0,
|
|
status: "Scheduled",
|
|
flyingTime: "10h 30m",
|
|
updated: "2025-01-15T10:00:00Z",
|
|
dayChange: 0,
|
|
equipment: { name: "Boeing 777-300ER", code: "773" },
|
|
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
|
|
operatingBy: {},
|
|
departure: {
|
|
scheduled: {
|
|
airport: "Sheremetyevo International Airport",
|
|
airportCode: "SVO",
|
|
city: "Moscow",
|
|
cityCode: "MOW",
|
|
countryCode: "RU",
|
|
},
|
|
checkingStatus: "closed",
|
|
times: {
|
|
scheduledDeparture: {
|
|
dayChange: { value: 0, title: "" },
|
|
local: "2025-01-15T10:00:00",
|
|
localTime: "10:00",
|
|
tzOffset: 3,
|
|
utc: "2025-01-15T07:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
arrival: {
|
|
scheduled: {
|
|
airport: "John F. Kennedy International Airport",
|
|
airportCode: "JFK",
|
|
city: "New York",
|
|
cityCode: "NYC",
|
|
countryCode: "US",
|
|
},
|
|
times: {
|
|
scheduledArrival: {
|
|
dayChange: { value: 0, title: "" },
|
|
local: "2025-01-15T14:30:00",
|
|
localTime: "14:30",
|
|
tzOffset: -5,
|
|
utc: "2025-01-15T19:30:00Z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
it("uses FLIGHT-DETAILS translation keys", () => {
|
|
const result = buildFlightDetailsSeo(stubT, flight, "ru", CANONICAL);
|
|
|
|
expect(result.title).toContain("SEO.BOARD.FLIGHT-DETAILS.TITLE");
|
|
expect(result.title).toContain("flightNumber=SU 0100");
|
|
});
|
|
|
|
it("sets canonical to the details URL", () => {
|
|
const result = buildFlightDetailsSeo(stubT, flight, "ru", CANONICAL);
|
|
|
|
expect(result.canonical).toBe(
|
|
"https://www.aeroflot.ru/ru/onlineboard/SU0100-20250115",
|
|
);
|
|
});
|
|
|
|
it("includes hreflang entries", () => {
|
|
const result = buildFlightDetailsSeo(stubT, flight, "ru", CANONICAL);
|
|
|
|
expect(result.hreflang).toHaveLength(10);
|
|
});
|
|
|
|
it("sets og.type to article for details pages", () => {
|
|
const result = buildFlightDetailsSeo(stubT, flight, "ru", CANONICAL);
|
|
|
|
expect(result.og.type).toBe("article");
|
|
});
|
|
});
|