Enforce [-1, +14] date-window guard on Online-Board route route per TZ 4.1.2-R11

Out-of-window dates now redirect to /{locale}/onlineboard instead of rendering
stale data. Parse failures (malformed URLs) continue to 404 unchanged.
This commit is contained in:
2026-04-21 17:00:07 +03:00
parent f5304e200e
commit f2c52ca988
2 changed files with 114 additions and 1 deletions
@@ -0,0 +1,109 @@
/**
* Tests for the route search Online-Board page.
*
* Verifies that out-of-window dates redirect to /{locale}/onlineboard (TZ §4.1.2),
* in-window dates render normally, and malformed URLs still produce 404.
*
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import React from "react";
// Freeze clock: today = 2026-05-15. Board window: 2026-05-14 … 2026-05-29.
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 4, 15, 12));
});
afterEach(() => {
vi.useRealTimers();
});
// --- mocks ---
vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "ru" } }),
}));
vi.mock("@/env/index.js", () => ({
getEnv: () => ({ PROD_ORIGIN: "https://aeroflot.ru" }),
}));
vi.mock("@/ui/seo/SeoHead.js", () => ({
SeoHead: () => <div data-testid="seo-head" />,
}));
vi.mock("@/ui/errors/ErrorPage.js", () => ({
ErrorPage: ({ code }: { code: string }) => <div data-testid={`error-${code}`} />,
}));
vi.mock("@/ui/flights/FlightListSkeleton.js", () => ({
FlightListSkeleton: () => <div data-testid="skeleton" />,
}));
vi.mock(
"@/features/online-board/components/OnlineBoardSearchPage.js",
() => ({ OnlineBoardSearchPage: () => <div data-testid="board-search" /> }),
);
vi.mock("@/features/online-board/seo.js", () => ({
buildRouteSearchSeo: () => ({}),
}));
const mockNavigate = vi.fn();
vi.mock("@modern-js/runtime/router", () => ({
useParams: vi.fn(),
Navigate: ({ to }: { to: string }) => {
mockNavigate(to);
return <div data-testid="navigate" data-to={to} />;
},
}));
import { useParams } from "@modern-js/runtime/router";
import RouteSearchPage from "./page.js";
function renderWithParams(params: string, lang = "ru-ru") {
vi.mocked(useParams).mockReturnValue({ params, lang });
return render(<RouteSearchPage />);
}
describe("RouteSearchPage — date-window guard", () => {
beforeEach(() => {
mockNavigate.mockReset();
});
it("renders normally for an in-window date (today)", () => {
renderWithParams("SVO-LED-20260515");
expect(screen.getByTestId("seo-head")).toBeTruthy();
expect(screen.queryByTestId("navigate")).toBeNull();
});
it("renders normally for the earliest allowed date (today - 1)", () => {
renderWithParams("SVO-LED-20260514");
expect(screen.queryByTestId("navigate")).toBeNull();
});
it("redirects when date is too late (today + 15)", () => {
renderWithParams("SVO-LED-20260530");
expect(screen.getByTestId("navigate")).toBeTruthy();
expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/onlineboard");
});
it("redirects when date is too early (today - 2)", () => {
renderWithParams("SVO-LED-20260513");
expect(screen.getByTestId("navigate")).toBeTruthy();
expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/onlineboard");
});
it("uses the route locale in the redirect path", () => {
renderWithParams("SVO-LED-20260530", "en-us");
expect(mockNavigate).toHaveBeenCalledWith("/en-us/onlineboard");
});
it("shows 404 for a malformed URL (no date)", () => {
renderWithParams("SVO-LED");
expect(screen.getByTestId("error-404")).toBeTruthy();
expect(screen.queryByTestId("navigate")).toBeNull();
});
});
@@ -6,7 +6,7 @@
*/
import { lazy, Suspense } from "react";
import { useParams } from "@modern-js/runtime/router";
import { useParams, Navigate } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import { parseRouteUrlParams } from "@/features/online-board/url.js";
import { buildRouteSearchSeo } from "@/features/online-board/seo.js";
@@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
import { getEnv } from "@/env/index.js";
import { boardDateRedirect } from "../../_guards.js";
const OnlineBoardSearchPage = lazy(() =>
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
@@ -30,6 +31,9 @@ export default function RouteSearchPage(): JSX.Element {
if (!parsed) return <ErrorPage code="404" />;
const redirect = boardDateRedirect(locale, parsed.date);
if (redirect) return <Navigate to={redirect} replace />;
const canonicalOrigin = getEnv().PROD_ORIGIN;
const searchParams = {
type: "route" as const,