Allow schedule weeks at date window edges

This commit is contained in:
2026-05-14 19:15:27 +03:00
parent 147183ef90
commit 0284372385
7 changed files with 150 additions and 11 deletions
+8 -1
View File
@@ -3,6 +3,10 @@ import { moduleFederationPlugin } from "@module-federation/modern-js";
const buildTarget = process.env["BUILD_TARGET"];
const isRemote = buildTarget === "remote";
const plugins = [
appTools({ bundler: "rspack" }),
...(isRemote ? [moduleFederationPlugin()] : []),
];
// Runtime env values that must reach the client bundle. Rspack resolves
// `process.env` at BUILD time to an empty-ish polyfill in the browser, so
@@ -35,7 +39,7 @@ const PUBLIC_ENV_SCRIPT =
`window.__ENV__=Object.assign(window.__ENV__||Object.create(null),JSON.parse(atob("${publicEnvB64}")));`;
export default defineConfig({
plugins: [appTools({ bundler: "rspack" }), moduleFederationPlugin()],
plugins,
source: {
entriesDir: "./src",
},
@@ -95,6 +99,9 @@ export default defineConfig({
},
},
tools: {
rspack(config) {
config.cache = false;
},
cssLoader: {
url: false,
},
+2 -2
View File
@@ -39,11 +39,11 @@ console.log(`Starting Modern.js on :${MODERNJS_PORT}...`);
const modernBin = resolve("node_modules", ".bin", "modern");
const modernProcess = existsSync(modernBin)
? spawn(modernBin, ["dev"], {
stdio: "inherit",
stdio: ["ignore", "inherit", "inherit"],
env: { ...process.env, PORT: String(MODERNJS_PORT) },
})
: spawn(process.execPath, [resolve("node_modules", "@modern-js/app-tools", "bin", "modern.js"), "dev"], {
stdio: "inherit",
stdio: ["ignore", "inherit", "inherit"],
env: { ...process.env, PORT: String(MODERNJS_PORT) },
});
modernProcess.on("error", (err) => {
@@ -46,4 +46,26 @@ describe("scheduleDateRedirect (clock frozen 2026-05-15)", () => {
// 2026-05-15 + 100 = 2026-08-23
expect(scheduleDateRedirect("ru-ru", "20260823")).toBeNull();
});
it("allows the first schedule week when the range starts before the strict window", () => {
// Window starts on Thu 2026-05-14; Angular accepts the containing Mon-Sun week.
expect(scheduleDateRedirect("ru-ru", "20260511", "20260517")).toBeNull();
});
it("allows the last schedule week when the range ends after the strict window", () => {
// Window ends on Sat 2027-04-10; Angular accepts the containing Mon-Sun week.
expect(scheduleDateRedirect("ru-ru", "20270405", "20270411")).toBeNull();
});
it("redirects a range before the first schedule week", () => {
expect(scheduleDateRedirect("ru-ru", "20260504", "20260510")).toBe("/ru-ru/schedule");
});
it("redirects a range after the last schedule week", () => {
expect(scheduleDateRedirect("ru-ru", "20270412", "20270418")).toBe("/ru-ru/schedule");
});
it("redirects an inverted date range", () => {
expect(scheduleDateRedirect("ru-ru", "20260518", "20260517")).toBe("/ru-ru/schedule");
});
});
+45 -5
View File
@@ -5,15 +5,55 @@
* Parse failures (malformed URLs) continue to produce 404 via existing logic.
*/
import { isInScheduleWindow } from "@/shared/dateWindow.js";
import { isInScheduleWindow, scheduleWindowBounds } from "@/shared/dateWindow.js";
function parseYyyymmdd(s: string): Date | null {
if (!/^\d{8}$/.test(s)) return null;
const y = Number(s.slice(0, 4));
const m = Number(s.slice(4, 6));
const d = Number(s.slice(6, 8));
const dt = new Date(y, m - 1, d);
if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;
dt.setHours(0, 0, 0, 0);
return dt;
}
function startOfWeekMonday(date: Date): Date {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
const offset = (d.getDay() + 6) % 7;
d.setDate(d.getDate() - offset);
return d;
}
function endOfWeekSunday(date: Date): Date {
const d = startOfWeekMonday(date);
d.setDate(d.getDate() + 6);
return d;
}
function isScheduleRangeAllowed(dateFrom: string, dateTo: string): boolean {
const from = parseYyyymmdd(dateFrom);
const to = parseYyyymmdd(dateTo);
if (!from || !to || from.getTime() > to.getTime()) return false;
const [windowMin, windowMax] = scheduleWindowBounds();
const minWeekStart = startOfWeekMonday(windowMin);
const maxWeekEnd = endOfWeekSunday(windowMax);
return from.getTime() >= minWeekStart.getTime() && to.getTime() <= maxWeekEnd.getTime();
}
/**
* Returns the redirect target path when the given yyyymmdd date falls outside
* the Schedule [-1, +330] day window. Returns null to allow normal render.
* Returns the redirect target path when the given yyyymmdd date/range falls
* outside the Schedule [-1, +330] day window. Week-granular schedule ranges
* are allowed when they overlap the first or last valid week, matching Angular.
* Returns null to allow normal render.
*
* Only called after a successful URL parse — malformed dates never reach here.
*/
export function scheduleDateRedirect(locale: string, yyyymmdd: string): string | null {
if (!isInScheduleWindow(yyyymmdd)) return `/${locale}/schedule`;
export function scheduleDateRedirect(locale: string, dateFrom: string, dateTo?: string): string | null {
const isAllowed = dateTo ? isScheduleRangeAllowed(dateFrom, dateTo) : isInScheduleWindow(dateFrom);
if (!isAllowed) return `/${locale}/schedule`;
return null;
}
@@ -42,8 +42,8 @@ export default function ScheduleRoundTripSearchPage(): JSX.Element {
if (!outbound || !inbound) return <ErrorPage code="404" />;
const redirect =
scheduleDateRedirect(locale, outbound.dateFrom) ??
scheduleDateRedirect(locale, inbound.dateFrom);
scheduleDateRedirect(locale, outbound.dateFrom, outbound.dateTo) ??
scheduleDateRedirect(locale, inbound.dateFrom, inbound.dateTo);
if (redirect) return <Navigate to={redirect} replace />;
const canonicalOrigin = getEnv().PROD_ORIGIN;
@@ -38,7 +38,7 @@ export default function ScheduleRouteSearchPage(): JSX.Element {
if (!parsed) return <ErrorPage code="404" />;
const redirect = scheduleDateRedirect(locale, parsed.dateFrom);
const redirect = scheduleDateRedirect(locale, parsed.dateFrom, parsed.dateTo);
if (redirect) return <Navigate to={redirect} replace />;
const canonicalOrigin = getEnv().PROD_ORIGIN;
@@ -0,0 +1,70 @@
import { test, expect } from "./fixtures/console-gate";
function addDays(base: Date, days: number): Date {
const d = new Date(base);
d.setDate(d.getDate() + days);
return d;
}
function startOfWeekMonday(base: Date): Date {
const d = new Date(base);
d.setHours(0, 0, 0, 0);
const offset = (d.getDay() + 6) % 7;
d.setDate(d.getDate() - offset);
return d;
}
function yyyymmdd(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}${m}${d}`;
}
function currentScheduleWeekRange(): [string, string] {
const scheduleMinDate = addDays(new Date(), -1);
const monday = startOfWeekMonday(scheduleMinDate);
return [yyyymmdd(monday), yyyymmdd(addDays(monday, 6))];
}
function nextScheduleWeekRange(): [string, string] {
const [currentWeekStart] = currentScheduleWeekRange();
const monday = startOfWeekMonday(
new Date(
Number(currentWeekStart.slice(0, 4)),
Number(currentWeekStart.slice(4, 6)) - 1,
Number(currentWeekStart.slice(6, 8)),
),
);
const nextMonday = addDays(monday, 7);
return [yyyymmdd(nextMonday), yyyymmdd(addDays(nextMonday, 6))];
}
test.describe("Schedule VVO-MJZ week route parity", () => {
test("current schedule week route does not reset to the start page", async ({
page,
consoleMessages,
}) => {
const [dateFrom, dateTo] = currentScheduleWeekRange();
await page.goto(`/ru-ru/schedule/route/VVO-MJZ-${dateFrom}-${dateTo}`);
await expect(page.locator("h1")).toContainText(/(Владивосток.*Мирный|VVO.*MJZ)/, {
timeout: 30000,
});
await expect(page).toHaveURL(new RegExp(`/schedule/route/VVO-MJZ-${dateFrom}-${dateTo}`));
});
test("next schedule week route renders without the search error page", async ({
page,
consoleMessages,
}) => {
const [dateFrom, dateTo] = nextScheduleWeekRange();
await page.goto(`/ru-ru/schedule/route/VVO-MJZ-${dateFrom}-${dateTo}`);
await expect(page.locator("h1")).toContainText(/(Владивосток.*Мирный|VVO.*MJZ)/, {
timeout: 30000,
});
await expect(page.getByText("Что-то пошло не так")).toBeHidden();
await expect(page).toHaveURL(new RegExp(`/schedule/route/VVO-MJZ-${dateFrom}-${dateTo}`));
});
});