Allow schedule weeks at date window edges
This commit is contained in:
+8
-1
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user