diff --git a/src/features/flights-map/seo.test.ts b/src/features/flights-map/seo.test.ts index aac2e411..b7b5d211 100644 --- a/src/features/flights-map/seo.test.ts +++ b/src/features/flights-map/seo.test.ts @@ -104,3 +104,74 @@ describe("buildFlightsMapJsonLd", () => { expect(result.name).toContain("Flight Map"); }); }); + +// --------------------------------------------------------------------------- +// TZ §4.1.19 — OpenGraph completeness assertions (Flights Map) +// --------------------------------------------------------------------------- + +describe("4.1.19-R: OpenGraph completeness (Flights Map)", () => { + const ORIGIN = "https://flights.aeroflot.ru"; + + it("4.1.19-R: Flights Map page has all required OG tags", () => { + const seo = buildFlightsMapSeo(stubT, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); +}); + +// --------------------------------------------------------------------------- +// TZ §4.1.20 — Robots, canonical, hreflang assertions (Flights Map) +// --------------------------------------------------------------------------- + +describe("4.1.20-R: canonical + hreflang (Flights Map)", () => { + const ORIGIN = "https://flights.aeroflot.ru"; + + it("4.1.20-R: Flights Map canonical has no query params", () => { + const seo = buildFlightsMapSeo(stubT, "ru", ORIGIN); + + expect(seo.canonical).not.toContain("?"); + expect(seo.canonical).toBe(`${ORIGIN}/ru/flights-map`); + }); + + it("4.1.20-R: hreflang has all 9 languages + x-default", () => { + const seo = buildFlightsMapSeo(stubT, "ru", ORIGIN); + + expect(seo.hreflang.length).toBeGreaterThanOrEqual(9); + expect(seo.hreflang.some((h) => h.lang === "x-default")).toBe(true); + }); + + it("4.1.20-R: hreflang covers all 9 supported languages", () => { + const seo = buildFlightsMapSeo(stubT, "ru", ORIGIN); + const langs = seo.hreflang.map((h) => h.lang); + + expect(langs).toContain("ru"); + expect(langs).toContain("en"); + expect(langs).toContain("es"); + expect(langs).toContain("fr"); + expect(langs).toContain("it"); + expect(langs).toContain("ja"); + expect(langs).toContain("ko"); + expect(langs).toContain("zh"); + expect(langs).toContain("de"); + }); + + it("4.1.20-R: hreflang hrefs are absolute URLs", () => { + const seo = buildFlightsMapSeo(stubT, "ru", ORIGIN); + + for (const entry of seo.hreflang) { + expect(entry.href).toMatch(/^https:\/\//); + } + }); + + it("4.1.20-R: noindex is not set on the Flights Map page (indexable)", () => { + const seo = buildFlightsMapSeo(stubT, "ru", ORIGIN); + + expect(seo.noindex).toBeFalsy(); + }); +}); diff --git a/src/features/online-board/seo.test.ts b/src/features/online-board/seo.test.ts index 772cad65..d14c40a3 100644 --- a/src/features/online-board/seo.test.ts +++ b/src/features/online-board/seo.test.ts @@ -471,3 +471,152 @@ describe("buildFlightDetailsSeo", () => { expect(result.og.type).toBe("article"); }); }); + +// --------------------------------------------------------------------------- +// TZ §4.1.19 — OpenGraph completeness assertions (all required tags present) +// --------------------------------------------------------------------------- + +describe("4.1.19-R: OpenGraph completeness", () => { + const ORIGIN = "https://flights.aeroflot.ru"; + + it("4.1.19-R: Online-Board start page has all required OG tags", () => { + const seo = buildOnlineBoardStartSeo(stubT, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Online-Board flight search has all required OG tags", () => { + const params = { type: "flight" as const, carrier: "SU", flightNumber: "0100", date: "20250115" }; + const seo = buildFlightSearchSeo(stubT, params, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Online-Board departure search has all required OG tags", () => { + const params = { type: "departure" as const, station: "SVO", date: "20250115" }; + const seo = buildDepartureSearchSeo(stubT, params, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Online-Board arrival search has all required OG tags", () => { + const params = { type: "arrival" as const, station: "JFK", date: "20250115" }; + const seo = buildArrivalSearchSeo(stubT, params, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Online-Board route search has all required OG tags", () => { + const params = { type: "route" as const, departure: "SVO", arrival: "JFK", date: "20250115" }; + const seo = buildRouteSearchSeo(stubT, params, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Online-Board flight details has all required OG tags", () => { + const seo = buildFlightDetailsSeoFromId( + stubT, + { carrier: "SU", flightNumber: "0100", date: "20250115" }, + "ru", + ORIGIN, + ); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("article"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); +}); + +// --------------------------------------------------------------------------- +// TZ §4.1.20 — Robots, canonical, hreflang assertions +// --------------------------------------------------------------------------- + +describe("4.1.20-R: canonical + hreflang", () => { + const ORIGIN = "https://flights.aeroflot.ru"; + + it("4.1.20-R: canonical URL is normalized (no query params)", () => { + const seo = buildOnlineBoardStartSeo(stubT, "ru", ORIGIN); + + expect(seo.canonical).not.toContain("?"); + expect(seo.canonical).toBe(`${ORIGIN}/ru/onlineboard`); + }); + + it("4.1.20-R: hreflang has all 9 languages + x-default", () => { + const seo = buildOnlineBoardStartSeo(stubT, "ru", ORIGIN); + + expect(seo.hreflang.length).toBeGreaterThanOrEqual(9); + expect(seo.hreflang.some((h) => h.lang === "x-default")).toBe(true); + }); + + it("4.1.20-R: hreflang covers all 9 supported languages", () => { + const seo = buildOnlineBoardStartSeo(stubT, "ru", ORIGIN); + const langs = seo.hreflang.map((h) => h.lang); + + expect(langs).toContain("ru"); + expect(langs).toContain("en"); + expect(langs).toContain("es"); + expect(langs).toContain("fr"); + expect(langs).toContain("it"); + expect(langs).toContain("ja"); + expect(langs).toContain("ko"); + expect(langs).toContain("zh"); + expect(langs).toContain("de"); + }); + + it("4.1.20-R: hreflang hrefs are absolute URLs", () => { + const seo = buildOnlineBoardStartSeo(stubT, "ru", ORIGIN); + + for (const entry of seo.hreflang) { + expect(entry.href).toMatch(/^https:\/\//); + } + }); + + it("4.1.20-R: flight search canonical has no query params", () => { + const params = { type: "flight" as const, carrier: "SU", flightNumber: "0100", date: "20250115" }; + const seo = buildFlightSearchSeo(stubT, params, "ru", ORIGIN); + + expect(seo.canonical).not.toContain("?"); + expect(seo.canonical).toMatch(/^https:\/\//); + }); + + it("4.1.20-R: noindex is not set on standard indexable pages (defaults to index,follow)", () => { + const seo = buildOnlineBoardStartSeo(stubT, "ru", ORIGIN); + + // Standard pages must be indexable — noindex prop must be absent/falsy + expect(seo.noindex).toBeFalsy(); + }); +}); diff --git a/src/features/schedule/seo.test.ts b/src/features/schedule/seo.test.ts index df4386db..bdeb7bc7 100644 --- a/src/features/schedule/seo.test.ts +++ b/src/features/schedule/seo.test.ts @@ -367,3 +367,103 @@ describe("buildScheduleDetailsSeo", () => { expect(result.og.type).toBe("article"); }); }); + +// --------------------------------------------------------------------------- +// TZ §4.1.19 — OpenGraph completeness assertions +// --------------------------------------------------------------------------- + +describe("4.1.19-R: OpenGraph completeness (Schedule)", () => { + const ORIGIN = "https://flights.aeroflot.ru"; + + it("4.1.19-R: Schedule start page has all required OG tags", () => { + const seo = buildScheduleStartSeo(stubT, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Schedule search page has all required OG tags", () => { + const params = { + type: "route" as const, + outbound: { departure: "MOW", arrival: "LED", dateFrom: "20250115", dateTo: "20250120" }, + }; + const seo = buildScheduleSearchSeo(stubT, params, "ru", ORIGIN); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("website"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); + + it("4.1.19-R: Schedule details page has all required OG tags", () => { + const fIds: IScheduleFlightId[] = [{ carrier: "SU", flightNumber: "0012", date: "20220527" }]; + const seo = buildScheduleDetailsSeo(stubT, [], "ru", ORIGIN, fIds); + + expect(seo.og.title).toBeTruthy(); + expect(seo.og.description).toBeTruthy(); + expect(seo.og.url).toMatch(/^https:\/\//); + expect(seo.og.image).toMatch(/^https:\/\//); + expect(seo.og.type).toBe("article"); + expect(seo.og.locale).toBe("ru"); + expect(seo.og.siteName).toBe("Aeroflot"); + }); +}); + +// --------------------------------------------------------------------------- +// TZ §4.1.20 — Robots, canonical, hreflang assertions (Schedule) +// --------------------------------------------------------------------------- + +describe("4.1.20-R: canonical + hreflang (Schedule)", () => { + const ORIGIN = "https://flights.aeroflot.ru"; + + it("4.1.20-R: Schedule start canonical has no query params", () => { + const seo = buildScheduleStartSeo(stubT, "ru", ORIGIN); + + expect(seo.canonical).not.toContain("?"); + expect(seo.canonical).toBe(`${ORIGIN}/ru/schedule`); + }); + + it("4.1.20-R: hreflang has all 9 languages + x-default", () => { + const seo = buildScheduleStartSeo(stubT, "ru", ORIGIN); + + expect(seo.hreflang.length).toBeGreaterThanOrEqual(9); + expect(seo.hreflang.some((h) => h.lang === "x-default")).toBe(true); + }); + + it("4.1.20-R: hreflang covers all 9 supported languages", () => { + const seo = buildScheduleStartSeo(stubT, "ru", ORIGIN); + const langs = seo.hreflang.map((h) => h.lang); + + expect(langs).toContain("ru"); + expect(langs).toContain("en"); + expect(langs).toContain("es"); + expect(langs).toContain("fr"); + expect(langs).toContain("it"); + expect(langs).toContain("ja"); + expect(langs).toContain("ko"); + expect(langs).toContain("zh"); + expect(langs).toContain("de"); + }); + + it("4.1.20-R: hreflang hrefs are absolute URLs", () => { + const seo = buildScheduleStartSeo(stubT, "ru", ORIGIN); + + for (const entry of seo.hreflang) { + expect(entry.href).toMatch(/^https:\/\//); + } + }); + + it("4.1.20-R: noindex is not set on standard indexable Schedule pages", () => { + const seo = buildScheduleStartSeo(stubT, "ru", ORIGIN); + + expect(seo.noindex).toBeFalsy(); + }); +});