This commit is contained in:
+12
-1
@@ -169,7 +169,18 @@ function isSuccessfulUpstream({ err, stdout }) {
|
|||||||
const lastNewline = stdout.lastIndexOf("\n");
|
const lastNewline = stdout.lastIndexOf("\n");
|
||||||
const statusStr = lastNewline >= 0 ? stdout.substring(lastNewline + 1).trim() : "";
|
const statusStr = lastNewline >= 0 ? stdout.substring(lastNewline + 1).trim() : "";
|
||||||
const status = parseInt(statusStr) || 0;
|
const status = parseInt(statusStr) || 0;
|
||||||
return status >= 200 && status < 400;
|
if (status < 200 || status >= 400) return false;
|
||||||
|
|
||||||
|
const body = lastNewline >= 0 ? stdout.substring(0, lastNewline) : stdout;
|
||||||
|
const trimmed = body.trimStart();
|
||||||
|
// Some upstream edges return the Angular shell as HTTP 200 for API
|
||||||
|
// paths on the direct transport. Treat that as a failed API response so
|
||||||
|
// the fallback transport can fetch the JSON payload.
|
||||||
|
if (trimmed.startsWith("<!DOCTYPE html") || trimmed.startsWith("<html")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function respondWithCurlResult({ err, stdout }, res) {
|
function respondWithCurlResult({ err, stdout }, res) {
|
||||||
|
|||||||
@@ -232,6 +232,19 @@ describe("getScheduleCalendarDays", () => {
|
|||||||
expect(result).toEqual(["2025-01-15", "2025-01-16", "2025-01-17"]);
|
expect(result).toEqual(["2025-01-15", "2025-01-16", "2025-01-17"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("parses bitmask days from the requested base date", async () => {
|
||||||
|
const { client } = createMockClient({ days: "101" });
|
||||||
|
|
||||||
|
const result = await getScheduleCalendarDays(client, {
|
||||||
|
date: "2025-01-15",
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
connections: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(["2025-01-15", "2025-01-17"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns empty array for empty days string", async () => {
|
it("returns empty array for empty days string", async () => {
|
||||||
const { client } = createMockClient({ days: "" });
|
const { client } = createMockClient({ days: "" });
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ export async function getScheduleDetails(
|
|||||||
* Maps to: `GET days/{date}/382/{param}/schedule/v1`
|
* Maps to: `GET days/{date}/382/{param}/schedule/v1`
|
||||||
*
|
*
|
||||||
* The API returns `{ days: "1111110001..." }` — a 382-char bitmask
|
* The API returns `{ days: "1111110001..." }` — a 382-char bitmask
|
||||||
* where each character represents a day starting from (baseDate - 1).
|
* where each character represents a day starting from the requested
|
||||||
* '1' = available, '0' = no flights. Equivalent to the board endpoint.
|
* base date. '1' = available, '0' = no flights.
|
||||||
*/
|
*/
|
||||||
export async function getScheduleCalendarDays(
|
export async function getScheduleCalendarDays(
|
||||||
client: ApiClient,
|
client: ApiClient,
|
||||||
@@ -163,7 +163,6 @@ function bitmaskToDates(bitmask: string, baseDate: string): string[] {
|
|||||||
const [y, m, d] = iso.split("-");
|
const [y, m, d] = iso.split("-");
|
||||||
if (!y || !m || !d) return [];
|
if (!y || !m || !d) return [];
|
||||||
const cursor = new Date(Number(y), Number(m) - 1, Number(d));
|
const cursor = new Date(Number(y), Number(m) - 1, Number(d));
|
||||||
cursor.setDate(cursor.getDate() - 1);
|
|
||||||
|
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
for (let i = 0; i < bitmask.length; i++) {
|
for (let i = 0; i < bitmask.length; i++) {
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import { ScheduleFilter } from "./ScheduleFilter.js";
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const mockNavigate = vi.fn();
|
const mockNavigate = vi.fn();
|
||||||
|
const scheduleCalendarMock = vi.hoisted(() => ({
|
||||||
|
days: [] as string[],
|
||||||
|
loaded: false,
|
||||||
|
params: [] as unknown[],
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@modern-js/runtime/router", () => ({
|
vi.mock("@modern-js/runtime/router", () => ({
|
||||||
useNavigate: () => mockNavigate,
|
useNavigate: () => mockNavigate,
|
||||||
@@ -35,17 +40,29 @@ vi.mock("@/shared/dictionaries/index.js", () => ({
|
|||||||
// useScheduleCalendar would otherwise need the api provider in the test
|
// useScheduleCalendar would otherwise need the api provider in the test
|
||||||
// tree just for the disabled-dates wiring added in TIRREDESIGN-12.
|
// tree just for the disabled-dates wiring added in TIRREDESIGN-12.
|
||||||
vi.mock("../hooks/useScheduleCalendar.js", () => ({
|
vi.mock("../hooks/useScheduleCalendar.js", () => ({
|
||||||
useScheduleCalendar: () => ({ days: [], loading: false }),
|
useScheduleCalendar: (params: unknown) => {
|
||||||
|
scheduleCalendarMock.params.push(params);
|
||||||
|
return {
|
||||||
|
days: scheduleCalendarMock.days,
|
||||||
|
loading: false,
|
||||||
|
loaded: scheduleCalendarMock.loaded,
|
||||||
|
};
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// PrimeReact Calendar stub — read-only input so state is driven by props
|
// PrimeReact Calendar stub — read-only input so state is driven by props
|
||||||
vi.mock("primereact/calendar", () => ({
|
vi.mock("primereact/calendar", () => ({
|
||||||
Calendar: (props: Record<string, unknown>) => {
|
Calendar: (props: Record<string, unknown>) => {
|
||||||
const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
|
const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
|
||||||
|
const formatLocalYmd = (d: Date) =>
|
||||||
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
data-testid={props["data-testid"] as string}
|
data-testid={props["data-testid"] as string}
|
||||||
placeholder={props["placeholder"] as string}
|
placeholder={props["placeholder"] as string}
|
||||||
|
data-disabled-dates={((props["disabledDates"] as Date[] | undefined) ?? [])
|
||||||
|
.map(formatLocalYmd)
|
||||||
|
.join(",")}
|
||||||
readOnly
|
readOnly
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
@@ -96,6 +113,9 @@ vi.mock("@/shared/dateWindow.js", () => ({
|
|||||||
describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () => {
|
describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
scheduleCalendarMock.days = [];
|
||||||
|
scheduleCalendarMock.loaded = false;
|
||||||
|
scheduleCalendarMock.params = [];
|
||||||
_sliderOnChange = null;
|
_sliderOnChange = null;
|
||||||
_sliderOnChanges.length = 0;
|
_sliderOnChanges.length = 0;
|
||||||
});
|
});
|
||||||
@@ -196,6 +216,9 @@ describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () =
|
|||||||
describe("ScheduleFilter – time slider 1h minimum gap per TZ §4.1.9 Table 14", () => {
|
describe("ScheduleFilter – time slider 1h minimum gap per TZ §4.1.9 Table 14", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
scheduleCalendarMock.days = [];
|
||||||
|
scheduleCalendarMock.loaded = false;
|
||||||
|
scheduleCalendarMock.params = [];
|
||||||
_sliderOnChange = null;
|
_sliderOnChange = null;
|
||||||
_sliderOnChanges.length = 0;
|
_sliderOnChanges.length = 0;
|
||||||
});
|
});
|
||||||
@@ -238,9 +261,74 @@ describe("ScheduleFilter – time slider 1h minimum gap per TZ §4.1.9 Table 14"
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ScheduleFilter – operating days calendar parity (TIRREDESIGN-12)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
scheduleCalendarMock.days = [];
|
||||||
|
scheduleCalendarMock.loaded = false;
|
||||||
|
scheduleCalendarMock.params = [];
|
||||||
|
_sliderOnChange = null;
|
||||||
|
_sliderOnChanges.length = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests schedule days from the calendar minimum date", () => {
|
||||||
|
scheduleCalendarMock.days = ["2026-01-02"];
|
||||||
|
scheduleCalendarMock.loaded = true;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ScheduleFilter
|
||||||
|
initialDeparture="LED"
|
||||||
|
initialArrival="KUF"
|
||||||
|
initialDirectOnly={true}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(scheduleCalendarMock.params[0]).toMatchObject({
|
||||||
|
date: "2026-01-01",
|
||||||
|
departure: "LED",
|
||||||
|
arrival: "KUF",
|
||||||
|
connections: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables dates that are missing from the loaded operating-days response", () => {
|
||||||
|
scheduleCalendarMock.days = ["2026-01-02"];
|
||||||
|
scheduleCalendarMock.loaded = true;
|
||||||
|
|
||||||
|
render(<ScheduleFilter initialDeparture="LED" initialArrival="KUF" />);
|
||||||
|
|
||||||
|
const disabledDates = screen
|
||||||
|
.getByTestId("schedule-date-input")
|
||||||
|
.getAttribute("data-disabled-dates")
|
||||||
|
?.split(",");
|
||||||
|
|
||||||
|
expect(disabledDates).toContain("2026-01-01");
|
||||||
|
expect(disabledDates).not.toContain("2026-01-02");
|
||||||
|
expect(disabledDates).toContain("2026-01-03");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a loaded empty operating-days response as no available dates", () => {
|
||||||
|
scheduleCalendarMock.days = [];
|
||||||
|
scheduleCalendarMock.loaded = true;
|
||||||
|
|
||||||
|
render(<ScheduleFilter initialDeparture="LED" initialArrival="KUF" />);
|
||||||
|
|
||||||
|
const disabledDates = screen
|
||||||
|
.getByTestId("schedule-date-input")
|
||||||
|
.getAttribute("data-disabled-dates")
|
||||||
|
?.split(",");
|
||||||
|
|
||||||
|
expect(disabledDates).toContain("2026-01-01");
|
||||||
|
expect(disabledDates).toContain("2026-01-02");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("ScheduleFilter – validation per TZ §4.1.9.4", () => {
|
describe("ScheduleFilter – validation per TZ §4.1.9.4", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
scheduleCalendarMock.days = [];
|
||||||
|
scheduleCalendarMock.loaded = false;
|
||||||
|
scheduleCalendarMock.params = [];
|
||||||
_sliderOnChange = null;
|
_sliderOnChange = null;
|
||||||
_sliderOnChanges.length = 0;
|
_sliderOnChanges.length = 0;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,14 +71,6 @@ function yyyymmddToDate(yyyymmdd?: string): Date | null {
|
|||||||
return new Date(y, m, d);
|
return new Date(y, m, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
function todayIso(): string {
|
|
||||||
const d = new Date();
|
|
||||||
const y = d.getFullYear();
|
|
||||||
const m = (d.getMonth() + 1).toString().padStart(2, "0");
|
|
||||||
const day = d.getDate().toString().padStart(2, "0");
|
|
||||||
return `${y}-${m}-${day}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Date as `yyyy-MM-dd` — the shape returned by the schedule
|
* Format a Date as `yyyy-MM-dd` — the shape returned by the schedule
|
||||||
* `/calendar` API (`bitmaskToDates` in api.ts). Used only for the
|
* `/calendar` API (`bitmaskToDates` in api.ts). Used only for the
|
||||||
@@ -200,6 +192,10 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
|||||||
|
|
||||||
const scheduleMinDate = useRef(getScheduleMinDate()).current;
|
const scheduleMinDate = useRef(getScheduleMinDate()).current;
|
||||||
const scheduleMaxDate = useRef(getScheduleMaxDate()).current;
|
const scheduleMaxDate = useRef(getScheduleMaxDate()).current;
|
||||||
|
const scheduleCalendarBaseDate = useMemo(
|
||||||
|
() => dateToIsoYmd(scheduleMinDate),
|
||||||
|
[scheduleMinDate],
|
||||||
|
);
|
||||||
|
|
||||||
// TIRREDESIGN-12: fetch the 31-day operating-days bitmask for the
|
// TIRREDESIGN-12: fetch the 31-day operating-days bitmask for the
|
||||||
// selected route so PrimeReact greys out days without flights. The
|
// selected route so PrimeReact greys out days without flights. The
|
||||||
@@ -211,12 +207,12 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
|||||||
const arr = arrival.trim().toUpperCase();
|
const arr = arrival.trim().toUpperCase();
|
||||||
if (!dep || !arr || dep === arr) return null;
|
if (!dep || !arr || dep === arr) return null;
|
||||||
return {
|
return {
|
||||||
date: todayIso(),
|
date: scheduleCalendarBaseDate,
|
||||||
departure: dep,
|
departure: dep,
|
||||||
arrival: arr,
|
arrival: arr,
|
||||||
connections: !directOnly,
|
connections: !directOnly,
|
||||||
};
|
};
|
||||||
}, [departure, arrival, directOnly]);
|
}, [departure, arrival, directOnly, scheduleCalendarBaseDate]);
|
||||||
|
|
||||||
const returnCalendarParams = useMemo<IScheduleCalendarParams | null>(() => {
|
const returnCalendarParams = useMemo<IScheduleCalendarParams | null>(() => {
|
||||||
if (!returnFlights) return null;
|
if (!returnFlights) return null;
|
||||||
@@ -224,29 +220,35 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
|||||||
const arr = arrival.trim().toUpperCase();
|
const arr = arrival.trim().toUpperCase();
|
||||||
if (!dep || !arr || dep === arr) return null;
|
if (!dep || !arr || dep === arr) return null;
|
||||||
return {
|
return {
|
||||||
date: todayIso(),
|
date: scheduleCalendarBaseDate,
|
||||||
departure: arr,
|
departure: arr,
|
||||||
arrival: dep,
|
arrival: dep,
|
||||||
connections: !directOnly,
|
connections: !directOnly,
|
||||||
};
|
};
|
||||||
}, [departure, arrival, directOnly, returnFlights]);
|
}, [departure, arrival, directOnly, returnFlights, scheduleCalendarBaseDate]);
|
||||||
|
|
||||||
const { days: scheduleAvailableDays } = useScheduleCalendar(scheduleCalendarParams);
|
const {
|
||||||
const { days: returnAvailableDays } = useScheduleCalendar(returnCalendarParams);
|
days: scheduleAvailableDays,
|
||||||
|
loaded: scheduleCalendarLoaded,
|
||||||
|
} = useScheduleCalendar(scheduleCalendarParams);
|
||||||
|
const {
|
||||||
|
days: returnAvailableDays,
|
||||||
|
loaded: returnCalendarLoaded,
|
||||||
|
} = useScheduleCalendar(returnCalendarParams);
|
||||||
|
|
||||||
const scheduleDisabledDates = useMemo(
|
const scheduleDisabledDates = useMemo(
|
||||||
() =>
|
() =>
|
||||||
scheduleAvailableDays.length === 0
|
!scheduleCalendarLoaded
|
||||||
? []
|
? []
|
||||||
: computeDisabledDates(scheduleAvailableDays, scheduleMinDate, scheduleMaxDate),
|
: computeDisabledDates(scheduleAvailableDays, scheduleMinDate, scheduleMaxDate),
|
||||||
[scheduleAvailableDays, scheduleMinDate, scheduleMaxDate],
|
[scheduleAvailableDays, scheduleCalendarLoaded, scheduleMinDate, scheduleMaxDate],
|
||||||
);
|
);
|
||||||
const returnDisabledDates = useMemo(
|
const returnDisabledDates = useMemo(
|
||||||
() =>
|
() =>
|
||||||
returnAvailableDays.length === 0
|
!returnCalendarLoaded
|
||||||
? []
|
? []
|
||||||
: computeDisabledDates(returnAvailableDays, scheduleMinDate, scheduleMaxDate),
|
: computeDisabledDates(returnAvailableDays, scheduleMinDate, scheduleMaxDate),
|
||||||
[returnAvailableDays, scheduleMinDate, scheduleMaxDate],
|
[returnAvailableDays, returnCalendarLoaded, scheduleMinDate, scheduleMaxDate],
|
||||||
);
|
);
|
||||||
|
|
||||||
// §4.1.11 — submit button locked for 30 seconds after each search.
|
// §4.1.11 — submit button locked for 30 seconds after each search.
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ async function getCachedScheduleCalendarDays(
|
|||||||
export interface UseScheduleCalendarResult {
|
export interface UseScheduleCalendarResult {
|
||||||
days: string[];
|
days: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
loaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,28 +57,33 @@ export function useScheduleCalendar(
|
|||||||
const client = useApiClient();
|
const client = useApiClient();
|
||||||
const [days, setDays] = useState<string[]>([]);
|
const [days, setDays] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(Boolean(params));
|
const [loading, setLoading] = useState(Boolean(params));
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
setDays([]);
|
setDays([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoaded(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setLoaded(false);
|
||||||
|
|
||||||
getCachedScheduleCalendarDays(client, params)
|
getCachedScheduleCalendarDays(client, params)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setDays(result);
|
setDays(result);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoaded(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setDays([]);
|
setDays([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoaded(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,5 +98,5 @@ export function useScheduleCalendar(
|
|||||||
params?.connections,
|
params?.connections,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { days, loading };
|
return { days, loading, loaded };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,62 @@
|
|||||||
import { test, expect } from "./fixtures/console-gate";
|
import { test, expect } from "./fixtures/console-gate";
|
||||||
|
|
||||||
// TIRREDESIGN-12 — when both schedule cities are filled, the date-picker
|
// TIRREDESIGN-12 — when both schedule cities are filled, the date-picker
|
||||||
// must grey out the days the route does NOT operate. The fix in
|
// must grey out the days the route does NOT operate. The schedule
|
||||||
// ScheduleFilter.tsx aligned the date-format used for the
|
// `/days` bitmask is anchored to the requested calendar minimum date:
|
||||||
// available-days set lookup (yyyy-MM-dd, matching the schedule
|
// bit 0 maps to that exact date, not "base date minus one".
|
||||||
// `/days` API output) — previously the lookup compared `yyyymmdd`
|
|
||||||
// against `yyyy-MM-dd`, so every day was treated as unavailable
|
|
||||||
// and the entire calendar greyed out.
|
|
||||||
|
|
||||||
test("Schedule calendar greys out non-operating days for the route", async ({
|
function ymd(date: Date): string {
|
||||||
page,
|
return [
|
||||||
consoleMessages,
|
date.getFullYear(),
|
||||||
}) => {
|
String(date.getMonth() + 1).padStart(2, "0"),
|
||||||
await page.goto("/ru-ru/schedule/route/MOW-MMK-20260427-20260503");
|
String(date.getDate()).padStart(2, "0"),
|
||||||
await expect(page.locator(".day-grouped-flight-list").first()).toBeVisible({
|
].join("");
|
||||||
timeout: 15000,
|
}
|
||||||
|
|
||||||
|
function addDays(date: Date, days: number): Date {
|
||||||
|
const next = new Date(date);
|
||||||
|
next.setDate(next.getDate() + days);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Schedule calendar greys out non-operating days for the route", async ({ page }) => {
|
||||||
|
const minDate = addDays(new Date(), -1);
|
||||||
|
minDate.setHours(0, 0, 0, 0);
|
||||||
|
const dateTo = addDays(minDate, 6);
|
||||||
|
|
||||||
|
await page.route("**/api/flights/v1/*/days/**/schedule/", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ days: `0${"1".repeat(381)}` }),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.goto(`/ru-ru/schedule/route/LED-KUF-${ymd(minDate)}-${ymd(dateTo)}-C0`);
|
||||||
|
|
||||||
// Open the picker.
|
// Open the picker.
|
||||||
await page.locator("button.p-datepicker-trigger").first().click();
|
await page.locator("button.p-datepicker-trigger").first().click();
|
||||||
const panel = page.locator(".p-datepicker-panel, .p-datepicker").first();
|
const panel = page.locator(".p-datepicker-panel, .p-datepicker").first();
|
||||||
await expect(panel).toBeVisible();
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
// Wait for the operating-days API to come back (the disabled-set
|
const visibleDaySelector = ".p-datepicker td:not(.p-datepicker-other-month) span";
|
||||||
// is derived from it; before that, every day is enabled).
|
const minDateDay = String(minDate.getDate());
|
||||||
await expect
|
const nextDateDay = String(addDays(minDate, 1).getDate());
|
||||||
.poll(async () =>
|
|
||||||
page
|
|
||||||
.locator(".p-datepicker td span.p-disabled")
|
|
||||||
.count(),
|
|
||||||
{ timeout: 10000 })
|
|
||||||
.toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// The MOW→MMK route operates roughly daily today onward, so the
|
await expect.poll(async () => {
|
||||||
// visible month must contain BOTH enabled and disabled cells.
|
const cells = await page.locator(visibleDaySelector).evaluateAll((nodes) =>
|
||||||
// The check is intentionally loose because the live operating
|
nodes.map((node) => ({
|
||||||
// schedule shifts by route — but a fully-enabled or fully-disabled
|
text: node.textContent?.trim(),
|
||||||
// calendar would prove the format-mismatch regression returned.
|
className: node.className,
|
||||||
const enabled = await page
|
})),
|
||||||
.locator(".p-datepicker td span:not(.p-disabled)")
|
);
|
||||||
.count();
|
return cells.find((cell) => cell.text === minDateDay)?.className ?? "";
|
||||||
const disabled = await page
|
}, { timeout: 10000 }).toContain("p-disabled");
|
||||||
.locator(".p-datepicker td span.p-disabled")
|
|
||||||
.count();
|
const cells = await page.locator(visibleDaySelector).evaluateAll((nodes) =>
|
||||||
expect(enabled).toBeGreaterThan(0);
|
nodes.map((node) => ({
|
||||||
expect(disabled).toBeGreaterThan(0);
|
text: node.textContent?.trim(),
|
||||||
|
className: node.className,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
expect(cells.find((cell) => cell.text === nextDateDay)?.className ?? "").not.toContain("p-disabled");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user