Add schedule SEO and JSON-LD tests (Phase 3D)

Tests cover start page, search, and details SEO builders plus
Flight/ItemList JSON-LD schema generation for schedule pages.
This commit is contained in:
2026-04-15 09:28:04 +03:00
parent a072cd3bd2
commit 93f49cddae
3 changed files with 410 additions and 0 deletions
@@ -0,0 +1,9 @@
# Phase 3D -- Schedule SEO + JSON-LD Tests
> **Parent:** `2026-04-15-phase-3-schedule-master.md`
> **Depends on:** 3C (seo.ts, json-ld.ts already created)
## Deliverables
1. `src/features/schedule/seo.test.ts` -- SEO builder tests
2. `src/features/schedule/json-ld.test.ts` -- JSON-LD builder tests
+164
View File
@@ -0,0 +1,164 @@
import { describe, expect, it } from "vitest";
import type { Flight, ItemList } from "schema-dts";
import { buildScheduleFlightJsonLd, buildScheduleFlightListJsonLd } from "./json-ld.js";
import type { ISimpleFlight } from "./types.js";
function makeDirectFlight(overrides: Partial<{
carrier: string;
flightNumber: string;
depCode: string;
depAirport: string;
arrCode: string;
arrAirport: string;
depTime: string;
arrTime: string;
}> = {}): ISimpleFlight {
return {
id: "test-schedule-1",
routeType: "Direct",
flightId: {
carrier: overrides.carrier ?? "SU",
flightNumber: overrides.flightNumber ?? "0012",
suffix: "",
date: "20220527",
},
flyingTime: "1h 30m",
operatingBy: {},
status: "Scheduled",
leg: {
index: 0,
status: "Scheduled",
flyingTime: "1h 30m",
updated: "2022-05-27T10:00:00Z",
dayChange: 0,
equipment: { name: "Airbus A320", code: "320" },
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
operatingBy: {},
departure: {
scheduled: {
airport: overrides.depAirport ?? "Sheremetyevo",
airportCode: overrides.depCode ?? "SVO",
city: "Moscow",
cityCode: "MOW",
countryCode: "RU",
},
checkingStatus: "closed",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: overrides.depTime ?? "2022-05-27T10:00:00",
localTime: "10:00",
tzOffset: 3,
utc: "2022-05-27T07:00:00Z",
},
},
},
arrival: {
scheduled: {
airport: overrides.arrAirport ?? "Pulkovo",
airportCode: overrides.arrCode ?? "LED",
city: "Saint Petersburg",
cityCode: "LED",
countryCode: "RU",
},
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: overrides.arrTime ?? "2022-05-27T11:30:00",
localTime: "11:30",
tzOffset: 3,
utc: "2022-05-27T08:30:00Z",
},
},
},
},
};
}
describe("buildScheduleFlightJsonLd", () => {
it("returns a Flight schema with correct @type", () => {
const flight = makeDirectFlight();
const result = buildScheduleFlightJsonLd(flight);
expect(result["@type"]).toBe("Flight");
});
it("sets flightNumber in IATA format", () => {
const flight = makeDirectFlight({ carrier: "SU", flightNumber: "0012" });
const result = buildScheduleFlightJsonLd(flight);
expect(result.flightNumber).toBe("SU0012");
});
it("maps departure airport", () => {
const flight = makeDirectFlight({ depCode: "SVO", depAirport: "Sheremetyevo" });
const result = buildScheduleFlightJsonLd(flight);
expect(result.departureAirport).toEqual({
"@type": "Airport",
iataCode: "SVO",
name: "Sheremetyevo",
});
});
it("maps arrival airport", () => {
const flight = makeDirectFlight({ arrCode: "LED", arrAirport: "Pulkovo" });
const result = buildScheduleFlightJsonLd(flight);
expect(result.arrivalAirport).toEqual({
"@type": "Airport",
iataCode: "LED",
name: "Pulkovo",
});
});
it("is type-compatible with schema-dts Flight", () => {
const flight = makeDirectFlight();
const result = buildScheduleFlightJsonLd(flight);
const _typed: Flight = result;
expect(_typed["@type"]).toBe("Flight");
});
});
describe("buildScheduleFlightListJsonLd", () => {
it("returns an ItemList schema", () => {
const flights = [makeDirectFlight()];
const result = buildScheduleFlightListJsonLd(flights, "Schedule SVO to LED");
expect(result["@type"]).toBe("ItemList");
});
it("sets the description", () => {
const flights = [makeDirectFlight()];
const result = buildScheduleFlightListJsonLd(flights, "Schedule SVO to LED");
expect(result.description).toBe("Schedule SVO to LED");
});
it("wraps each flight as a ListItem with position", () => {
const flights = [
makeDirectFlight({ carrier: "SU", flightNumber: "0012" }),
makeDirectFlight({ carrier: "SU", flightNumber: "0013" }),
];
const result = buildScheduleFlightListJsonLd(flights, "Flights");
const items = result.itemListElement as unknown as Array<{ position: number }>;
expect(Array.isArray(items)).toBe(true);
expect(items).toHaveLength(2);
expect(items[0]).toHaveProperty("position", 1);
expect(items[1]).toHaveProperty("position", 2);
});
it("handles empty flight list", () => {
const result = buildScheduleFlightListJsonLd([], "No flights");
expect(result["@type"]).toBe("ItemList");
expect(result.itemListElement).toEqual([]);
});
it("is type-compatible with schema-dts ItemList", () => {
const result = buildScheduleFlightListJsonLd([], "test");
const _typed: ItemList = result;
expect(_typed["@type"]).toBe("ItemList");
});
});
+237
View File
@@ -0,0 +1,237 @@
import { describe, expect, it } from "vitest";
import {
buildScheduleStartSeo,
buildScheduleSearchSeo,
buildScheduleDetailsSeo,
} from "./seo.js";
import type { ISimpleFlight, IScheduleFlightId } from "./types.js";
import type { IFlightLeg } from "../online-board/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("buildScheduleStartSeo", () => {
it("uses MAIN translation keys", () => {
const result = buildScheduleStartSeo(stubT, "ru", CANONICAL);
expect(result.title).toBe("SEO.SCHEDULE.MAIN.TITLE");
expect(result.description).toBe("SEO.SCHEDULE.MAIN.DESCRIPTION");
});
it("sets canonical to /{locale}/schedule", () => {
const result = buildScheduleStartSeo(stubT, "en", CANONICAL);
expect(result.canonical).toBe("https://www.aeroflot.ru/en/schedule");
});
it("includes hreflang with 10 entries", () => {
const result = buildScheduleStartSeo(stubT, "ru", CANONICAL);
expect(result.hreflang).toHaveLength(10);
});
it("sets og tags", () => {
const result = buildScheduleStartSeo(stubT, "ru", CANONICAL);
expect(result.og.title).toBe(result.title);
expect(result.og.type).toBe("website");
expect(result.og.locale).toBe("ru");
expect(result.og.siteName).toBe("Aeroflot");
});
it("sets twitter card", () => {
const result = buildScheduleStartSeo(stubT, "ru", CANONICAL);
expect(result.twitter).toBeDefined();
expect(result.twitter!.card).toBe("summary");
});
});
describe("buildScheduleSearchSeo", () => {
const params = {
type: "route" as const,
outbound: {
departure: "MOW",
arrival: "KUF",
dateFrom: "20220425",
dateTo: "20220501",
},
};
it("uses SEARCH translation keys with interpolation", () => {
const result = buildScheduleSearchSeo(stubT, params, "ru", CANONICAL);
expect(result.title).toContain("SEO.SCHEDULE.SEARCH.TITLE");
expect(result.title).toContain("departureCity=MOW");
expect(result.title).toContain("arrivalCity=KUF");
expect(result.title).toContain("dateFrom=25.04.2022");
expect(result.title).toContain("dateTo=01.05.2022");
});
it("uses city names when provided", () => {
const result = buildScheduleSearchSeo(
stubT,
params,
"ru",
CANONICAL,
{ departure: "Moscow", arrival: "Samara" },
);
expect(result.title).toContain("departureCity=Moscow");
expect(result.title).toContain("arrivalCity=Samara");
});
it("sets canonical to the route search URL", () => {
const result = buildScheduleSearchSeo(stubT, params, "ru", CANONICAL);
expect(result.canonical).toBe(
"https://www.aeroflot.ru/ru/schedule/route/MOW-KUF-20220425-20220501",
);
});
it("includes hreflang entries", () => {
const result = buildScheduleSearchSeo(stubT, params, "ru", CANONICAL);
expect(result.hreflang).toHaveLength(10);
});
it("handles round-trip params", () => {
const rtParams = {
type: "roundtrip" as const,
outbound: {
departure: "MOW",
arrival: "KUF",
dateFrom: "20220425",
dateTo: "20220501",
},
inbound: {
departure: "KUF",
arrival: "MOW",
dateFrom: "20220502",
dateTo: "20220508",
},
};
const result = buildScheduleSearchSeo(stubT, rtParams, "ru", CANONICAL);
expect(result.canonical).toContain(
"schedule/route/MOW-KUF-20220425-20220501/KUF-MOW-20220502-20220508",
);
});
});
describe("buildScheduleDetailsSeo", () => {
const flightIds: IScheduleFlightId[] = [
{ carrier: "SU", flightNumber: "0012", date: "20220527" },
];
const flight: ISimpleFlight = {
id: "SU0012-20220527",
routeType: "Direct",
flightId: {
carrier: "SU",
flightNumber: "0012",
suffix: "",
date: "20220527",
},
flyingTime: "1h 30m",
operatingBy: {},
status: "Scheduled",
leg: {
index: 0,
status: "Scheduled",
flyingTime: "1h 30m",
updated: "2022-05-27T10:00:00Z",
dayChange: 0,
equipment: { name: "Airbus A320", code: "320" },
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
operatingBy: {},
departure: {
scheduled: {
airport: "Sheremetyevo",
airportCode: "SVO",
city: "Moscow",
cityCode: "MOW",
countryCode: "RU",
},
checkingStatus: "closed",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T10:00:00",
localTime: "10:00",
tzOffset: 3,
utc: "2022-05-27T07:00:00Z",
},
},
},
arrival: {
scheduled: {
airport: "Pulkovo",
airportCode: "LED",
city: "Saint Petersburg",
cityCode: "LED",
countryCode: "RU",
},
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T11:30:00",
localTime: "11:30",
tzOffset: 3,
utc: "2022-05-27T08:30:00Z",
},
},
},
},
};
it("uses DETAILS translation keys", () => {
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, flightIds);
expect(result.title).toContain("SEO.SCHEDULE.DETAILS.TITLE");
expect(result.title).toContain("flights=SU 0012");
expect(result.title).toContain("date=27.05.2022");
});
it("sets canonical to the details URL", () => {
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, flightIds);
expect(result.canonical).toBe(
"https://www.aeroflot.ru/ru/schedule/SU0012-20220527",
);
});
it("includes hreflang entries", () => {
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, flightIds);
expect(result.hreflang).toHaveLength(10);
});
it("sets og.type to article for details", () => {
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, flightIds);
expect(result.og.type).toBe("article");
});
it("handles multiple flights in display", () => {
const multiFlightIds: IScheduleFlightId[] = [
{ carrier: "SU", flightNumber: "0012", date: "20220527" },
{ carrier: "SU", flightNumber: "0013", date: "20220527" },
];
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, multiFlightIds);
expect(result.title).toContain("SU 0012, SU 0013");
});
});