Schedule date-picker: snap single click to Mon-Sun week + auto-close
Angular's schedule date-picker is week-granular (TZ §4.1.9.4): one click anywhere selects the whole calendar week, the panel closes and the input shows the resulting range. React was using PrimeReact's plain range-mode (two clicks required), so a single click left the range half-set and the panel open. Add snapToWeek() in ScheduleStartPage and ScheduleFilter, route both outbound + return Calendars through new onSelect handlers that compute Mon-Sun, commit it as the value, and call cal.hide() via ref. Enable selectOtherMonths so bleed-in days from the previous / next month are clickable. Add 3-test e2e spec (week snap from a mid-week day, snap from a next-month bleed-in day, range placeholder when empty).
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
import { type FC, useState, useCallback, useRef, useEffect, useMemo, type FormEvent } from "react";
|
||||
import { useNavigate } from "@modern-js/runtime/router";
|
||||
import { Calendar } from "primereact/calendar";
|
||||
import { Calendar, type CalendarChangeEvent } from "primereact/calendar";
|
||||
import { Slider, type SliderChangeEvent } from "primereact/slider";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { useLocale } from "@/i18n/useLocale.js";
|
||||
@@ -25,6 +25,25 @@ import { formatScheduleDateRangeWithCurrentWeek } from "../dateLabels.js";
|
||||
import { scheduleWindowBounds } from "@/shared/dateWindow.js";
|
||||
import "./ScheduleFilter.scss";
|
||||
|
||||
/**
|
||||
* Snap a date to the Mon-Sun week containing it (Angular parity:
|
||||
* the schedule filter selects whole weeks only). Returned tuple is
|
||||
* [Monday 00:00, Sunday 23:59:59.999].
|
||||
*/
|
||||
function snapToWeek(date: Date): [Date, Date] {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
// JS getDay: Sun=0, Mon=1 … Sat=6. Treat Mon as start-of-week.
|
||||
const day = d.getDay();
|
||||
const offsetToMonday = day === 0 ? -6 : 1 - day;
|
||||
const monday = new Date(d);
|
||||
monday.setDate(d.getDate() + offsetToMonday);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
return [monday, sunday];
|
||||
}
|
||||
|
||||
function minutesToTime(minutes: number): string {
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
@@ -257,6 +276,37 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
// current week. Uses inputRef + useEffect to override PrimeReact's
|
||||
// own dd.mm.yy rendering without touching the state value.
|
||||
const dateRangeInputRef = useRef<HTMLInputElement>(null);
|
||||
// Refs to the PrimeReact Calendar component instances so we can call
|
||||
// `hide()` after a single click commits the snap-to-week range
|
||||
// (matches Angular which closes the picker on selection).
|
||||
const outboundCalendarRef = useRef<Calendar | null>(null);
|
||||
const returnCalendarRef = useRef<Calendar | null>(null);
|
||||
|
||||
// Single-click handler for both outbound and return pickers: snap
|
||||
// the clicked day to its Mon-Sun calendar week, commit as a range,
|
||||
// and dismiss the panel. Angular parity (TZ §4.1.9.4 — the schedule
|
||||
// filter is week-granular).
|
||||
const onOutboundSelect = useCallback((e: CalendarChangeEvent): void => {
|
||||
const v = e.value;
|
||||
if (!v) return;
|
||||
const picked = Array.isArray(v) ? (v[0] ?? v[1]) : v;
|
||||
if (!(picked instanceof Date)) return;
|
||||
const [from, to] = snapToWeek(picked);
|
||||
setDateRange([from, to]);
|
||||
if (rangeError) setRangeError(null);
|
||||
outboundCalendarRef.current?.hide();
|
||||
}, [rangeError]);
|
||||
|
||||
const onReturnSelect = useCallback((e: CalendarChangeEvent): void => {
|
||||
const v = e.value;
|
||||
if (!v) return;
|
||||
const picked = Array.isArray(v) ? (v[0] ?? v[1]) : v;
|
||||
if (!(picked instanceof Date)) return;
|
||||
const [from, to] = snapToWeek(picked);
|
||||
setReturnDateRange([from, to]);
|
||||
setReturnBeforeOutboundError(null);
|
||||
returnCalendarRef.current?.hide();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const [from, to] = dateRange;
|
||||
if (!dateRangeInputRef.current || !from || !to) return;
|
||||
@@ -267,6 +317,32 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
}
|
||||
}, [dateRange, t]);
|
||||
|
||||
const returnDateRangeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// PrimeReact's `readOnlyInput=true` Calendar suppresses the auto-show
|
||||
// on input click; only the trigger icon opens the panel. Forward
|
||||
// input clicks to the icon so users can tap the field itself to open
|
||||
// the picker (matching Angular's calendar behaviour). Same logic
|
||||
// applied to both outbound and return date pickers.
|
||||
useEffect(() => {
|
||||
const cleanups: Array<() => void> = [];
|
||||
for (const ref of [dateRangeInputRef, returnDateRangeInputRef]) {
|
||||
const input = ref.current;
|
||||
if (!input) continue;
|
||||
const handler = (): void => {
|
||||
const wrapper = input.closest(".p-calendar");
|
||||
const trigger = wrapper?.querySelector<HTMLButtonElement>(
|
||||
".p-datepicker-trigger",
|
||||
);
|
||||
trigger?.click();
|
||||
};
|
||||
input.addEventListener("click", handler);
|
||||
input.style.cursor = "pointer";
|
||||
cleanups.push(() => input.removeEventListener("click", handler));
|
||||
}
|
||||
return () => cleanups.forEach((c) => c());
|
||||
}, []);
|
||||
|
||||
const handleSwap = useCallback(() => {
|
||||
setDeparture(arrival);
|
||||
setArrival(departure);
|
||||
@@ -472,15 +548,14 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
</label>
|
||||
<div className="calendar-input-wrapper">
|
||||
<Calendar
|
||||
ref={outboundCalendarRef}
|
||||
value={dateRange}
|
||||
onChange={(e) => {
|
||||
setDateRange((e.value as (Date | null)[]) ?? [null, null]);
|
||||
if (rangeError) setRangeError(null);
|
||||
}}
|
||||
onChange={onOutboundSelect}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
disabledDates={scheduleDisabledDates}
|
||||
selectOtherMonths
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
@@ -585,13 +660,9 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
</label>
|
||||
<div className="calendar-input-wrapper">
|
||||
<Calendar
|
||||
ref={returnCalendarRef}
|
||||
value={returnDateRange}
|
||||
onChange={(e) => {
|
||||
setReturnDateRange(
|
||||
(e.value as (Date | null)[]) ?? [null, null],
|
||||
);
|
||||
if (returnBeforeOutboundError) setReturnBeforeOutboundError(null);
|
||||
}}
|
||||
onChange={onReturnSelect}
|
||||
selectionMode="range"
|
||||
// TZ §4.1.9.4: return cannot start before outbound's
|
||||
// dateTo. Tie the return picker's minDate to it so
|
||||
@@ -600,12 +671,14 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
minDate={dateRange[1] ?? scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
disabledDates={returnDisabledDates}
|
||||
selectOtherMonths
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="schedule-return-date-input"
|
||||
inputId="schedule-return-date-input"
|
||||
inputRef={returnDateRangeInputRef}
|
||||
readOnlyInput
|
||||
/>
|
||||
{(returnDateRange[0] || returnDateRange[1]) && (
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { type FC, useState, useCallback, useRef, type FormEvent } from "react";
|
||||
import { type FC, useState, useCallback, useEffect, useRef, type FormEvent } from "react";
|
||||
import { useNavigate } from "@modern-js/runtime/router";
|
||||
import { useLocale } from "@/i18n/useLocale.js";
|
||||
import { Calendar } from "primereact/calendar";
|
||||
import { Calendar, type CalendarChangeEvent } from "primereact/calendar";
|
||||
import { Slider, type SliderChangeEvent } from "primereact/slider";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
@@ -19,15 +19,8 @@ import { SearchHistory } from "@/ui/layout/SearchHistory.js";
|
||||
import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
|
||||
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import {
|
||||
buildOnlineBoardPrefillState,
|
||||
ONLINE_BOARD_PREFILL_SLOT,
|
||||
SCHEDULE_PREFILL_SLOT,
|
||||
} from "@/features/online-board/components/OnlineBoardStartPage.js";
|
||||
import {
|
||||
readAndClearTransientPrefill,
|
||||
writeTransientPrefill,
|
||||
} from "@/shared/state/transientPrefill.js";
|
||||
import { SCHEDULE_PREFILL_SLOT } from "@/features/online-board/components/OnlineBoardStartPage.js";
|
||||
import { readAndClearTransientPrefill } from "@/shared/state/transientPrefill.js";
|
||||
import {
|
||||
getScheduleFilter,
|
||||
getBoardFilter,
|
||||
@@ -42,6 +35,7 @@ import type { IDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import { useGeoCityDefault } from "@/shared/hooks/useGeoCityDefault.js";
|
||||
import { buildScheduleUrl } from "../url.js";
|
||||
import { scheduleWindowBounds } from "@/shared/dateWindow.js";
|
||||
import { formatScheduleDateRangeWithCurrentWeek } from "../dateLabels.js";
|
||||
import "./ScheduleStartPage.scss";
|
||||
|
||||
function toCityCode(code: string, dictionaries: IDictionaries | null): string {
|
||||
@@ -122,6 +116,22 @@ function nextWeekBounds(base = new Date()): { from: Date; to: Date } {
|
||||
// Mirrors Angular AppSettings.scheduleSearchFrom (1 day back) and
|
||||
// scheduleSearchTo (330 days forward). Constrains both the outbound and
|
||||
// return-flight calendar pickers on the Schedule start page.
|
||||
/**
|
||||
* Snap a date to the Mon-Sun week containing it. Angular parity:
|
||||
* the schedule list is week-granular (TZ §4.1.9.4) — a single click
|
||||
* on any day in the picker commits the whole week.
|
||||
*/
|
||||
function snapToWeek(date: Date): { from: Date; to: Date } {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const dow = d.getDay() === 0 ? 7 : d.getDay();
|
||||
const mon = new Date(d);
|
||||
mon.setDate(d.getDate() - (dow - 1));
|
||||
const sun = new Date(mon);
|
||||
sun.setDate(mon.getDate() + 6);
|
||||
return { from: mon, to: sun };
|
||||
}
|
||||
|
||||
function getScheduleMinDate(): Date {
|
||||
return scheduleWindowBounds()[0];
|
||||
}
|
||||
@@ -236,6 +246,62 @@ export const ScheduleStartPage: FC = () => {
|
||||
const scheduleMinDate = useRef(getScheduleMinDate()).current;
|
||||
const scheduleMaxDate = useRef(getScheduleMaxDate()).current;
|
||||
|
||||
// TZ §4.1.9 Table 14 / Angular CalendarInputWeekComponent: when the
|
||||
// selected date range contains today, the Calendar input shows
|
||||
// "Текущая неделя" instead of the formatted dd.MM.yyyy-dd.MM.yyyy.
|
||||
// PrimeReact's Calendar always re-renders its own formatted value into
|
||||
// the input, so we override the .value via inputRef after each update.
|
||||
const dateRangeInputRef = useRef<HTMLInputElement>(null);
|
||||
const returnDateRangeInputRef = useRef<HTMLInputElement>(null);
|
||||
// Refs to the PrimeReact Calendar instances so we can call hide()
|
||||
// after a single-click commits the snap-to-week selection (Angular
|
||||
// closes its picker on selection — TZ §4.1.9.4).
|
||||
const outboundCalendarRef = useRef<Calendar | null>(null);
|
||||
const returnCalendarRef = useRef<Calendar | null>(null);
|
||||
|
||||
const onOutboundDateSelect = useCallback(
|
||||
(e: CalendarChangeEvent): void => {
|
||||
const v = e.value;
|
||||
if (!v) return;
|
||||
const picked = Array.isArray(v) ? (v[0] ?? v[1]) : v;
|
||||
if (!(picked instanceof Date)) return;
|
||||
const { from, to } = snapToWeek(picked);
|
||||
setDateFrom(from);
|
||||
setDateTo(to);
|
||||
outboundCalendarRef.current?.hide();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onReturnDateSelect = useCallback(
|
||||
(e: CalendarChangeEvent): void => {
|
||||
const v = e.value;
|
||||
if (!v) return;
|
||||
const picked = Array.isArray(v) ? (v[0] ?? v[1]) : v;
|
||||
if (!(picked instanceof Date)) return;
|
||||
const { from, to } = snapToWeek(picked);
|
||||
setReturnDateFrom(from);
|
||||
setReturnDateTo(to);
|
||||
returnCalendarRef.current?.hide();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dateRangeInputRef.current || !dateFrom || !dateTo) return;
|
||||
const label = formatScheduleDateRangeWithCurrentWeek(dateFrom, dateTo, t);
|
||||
if (label === t("SCHEDULE.CURRENT-WEEK")) {
|
||||
dateRangeInputRef.current.value = label;
|
||||
}
|
||||
}, [dateFrom, dateTo, t]);
|
||||
useEffect(() => {
|
||||
if (!returnDateRangeInputRef.current || !returnDateFrom || !returnDateTo) return;
|
||||
const label = formatScheduleDateRangeWithCurrentWeek(returnDateFrom, returnDateTo, t);
|
||||
if (label === t("SCHEDULE.CURRENT-WEEK")) {
|
||||
returnDateRangeInputRef.current.value = label;
|
||||
}
|
||||
}, [returnDateFrom, returnDateTo, t]);
|
||||
|
||||
// Same-cities validation mirrors ScheduleFilter (Angular's
|
||||
// ScheduleFilterValidationService): reject departure === arrival with
|
||||
// the shared translation key.
|
||||
@@ -317,46 +383,44 @@ export const ScheduleStartPage: FC = () => {
|
||||
|
||||
const handlePopularRequestClick = useCallback(
|
||||
(request: PopularRequest) => {
|
||||
// Popular-request entries sometimes carry airport codes (SVO, LED)
|
||||
// instead of city codes (MOW, LED). Normalize to the owning city
|
||||
// so clicks land on the city filter, not on a specific airport.
|
||||
if (request.type === "Onlineboard") {
|
||||
writeTransientPrefill(
|
||||
ONLINE_BOARD_PREFILL_SLOT,
|
||||
buildOnlineBoardPrefillState(request, dictionaries),
|
||||
);
|
||||
navigate(`/${locale}/onlineboard`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule-type: only Route / RouteWithBack carry city info.
|
||||
// TZ §4.1.5: prefill outbound dates = current ISO week; return dates
|
||||
// = next ISO week (only when withReturn = true).
|
||||
if (request.mode === "Route" || request.mode === "RouteWithBack") {
|
||||
// Deviation from Angular: every popular-request click on Schedule
|
||||
// populates the form in-place and stays on the page. Angular
|
||||
// redirects Arrival/Departure/FlightNumber clicks to /onlineboard
|
||||
// unconditionally; we instead treat the click as a Schedule prefill
|
||||
// because users land here to plan a trip, not to switch sections.
|
||||
// Codes sometimes arrive as airport (SVO/LED) — normalize to city.
|
||||
switch (request.mode) {
|
||||
case "Route":
|
||||
case "RouteWithBack": {
|
||||
const curWeek = currentWeekBounds();
|
||||
const state: SchedulePrefillState = {
|
||||
departure: toCityCode(request.departure, dictionaries),
|
||||
arrival: toCityCode(request.arrival, dictionaries),
|
||||
withReturn: request.mode === "RouteWithBack",
|
||||
dateFrom: dateToYyyymmdd(curWeek.from),
|
||||
dateTo: dateToYyyymmdd(curWeek.to),
|
||||
...(request.mode === "RouteWithBack"
|
||||
? (() => {
|
||||
setDepartureCode(toCityCode(request.departure, dictionaries));
|
||||
setArrivalCode(toCityCode(request.arrival, dictionaries));
|
||||
setIsRoundTrip(request.mode === "RouteWithBack");
|
||||
setDateFrom(curWeek.from);
|
||||
setDateTo(curWeek.to);
|
||||
if (request.mode === "RouteWithBack") {
|
||||
const nxt = nextWeekBounds();
|
||||
return {
|
||||
returnDateFrom: dateToYyyymmdd(nxt.from),
|
||||
returnDateTo: dateToYyyymmdd(nxt.to),
|
||||
};
|
||||
})()
|
||||
: {}),
|
||||
};
|
||||
writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state);
|
||||
setReturnDateFrom(nxt.from);
|
||||
setReturnDateTo(nxt.to);
|
||||
} else {
|
||||
writeTransientPrefill(SCHEDULE_PREFILL_SLOT, {});
|
||||
setReturnDateFrom(null);
|
||||
setReturnDateTo(null);
|
||||
}
|
||||
navigate(`/${locale}/schedule`);
|
||||
break;
|
||||
}
|
||||
case "Arrival":
|
||||
setArrivalCode(toCityCode(request.arrival, dictionaries));
|
||||
break;
|
||||
case "Departure":
|
||||
setDepartureCode(toCityCode(request.departure, dictionaries));
|
||||
break;
|
||||
case "FlightNumber":
|
||||
// Schedule has no flight-number field — ignore.
|
||||
break;
|
||||
}
|
||||
if (sameCitiesError) setSameCitiesError(null);
|
||||
},
|
||||
[navigate, locale, dictionaries],
|
||||
[dictionaries, sameCitiesError],
|
||||
);
|
||||
|
||||
const scheduleFilter = (
|
||||
@@ -419,20 +483,19 @@ export const ScheduleStartPage: FC = () => {
|
||||
<div className="schedule-start__field">
|
||||
<label htmlFor="schedule-date-from">{t("SHARED.SCHEDULES_DATE")}</label>
|
||||
<Calendar
|
||||
ref={outboundCalendarRef}
|
||||
value={dateFrom && dateTo ? [dateFrom, dateTo] : null}
|
||||
onChange={(e) => {
|
||||
const val = e.value as Date[] | null;
|
||||
if (val && val.length >= 1) setDateFrom(val[0] ?? null);
|
||||
if (val && val.length >= 2) setDateTo(val[1] ?? null);
|
||||
}}
|
||||
onChange={onOutboundDateSelect}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
selectOtherMonths
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
inputId="schedule-date-from"
|
||||
inputRef={dateRangeInputRef}
|
||||
data-testid="date-range-input"
|
||||
readOnlyInput
|
||||
/>
|
||||
@@ -486,20 +549,19 @@ export const ScheduleStartPage: FC = () => {
|
||||
<div className="schedule-start__field">
|
||||
<label htmlFor="schedule-return-date-range">{t("SHARED.RETURN_FLIGHT_DATE")}</label>
|
||||
<Calendar
|
||||
ref={returnCalendarRef}
|
||||
value={returnDateFrom && returnDateTo ? [returnDateFrom, returnDateTo] : null}
|
||||
onChange={(e) => {
|
||||
const val = e.value as Date[] | null;
|
||||
if (val && val.length >= 1) setReturnDateFrom(val[0] ?? null);
|
||||
if (val && val.length >= 2) setReturnDateTo(val[1] ?? null);
|
||||
}}
|
||||
onChange={onReturnDateSelect}
|
||||
selectionMode="range"
|
||||
minDate={scheduleMinDate}
|
||||
maxDate={scheduleMaxDate}
|
||||
selectOtherMonths
|
||||
dateFormat="dd.mm.yy"
|
||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||
showIcon
|
||||
className="input--filter"
|
||||
inputId="schedule-return-date-range"
|
||||
inputRef={returnDateRangeInputRef}
|
||||
data-testid="return-date-range-input"
|
||||
readOnlyInput
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// Schedule date picker — Angular parity (TZ §4.1.9.4):
|
||||
// • Single click on any day commits the **whole Mon-Sun week** that
|
||||
// contains it (the schedule list is week-granular).
|
||||
// • The panel auto-closes on selection.
|
||||
// • The text input shows the snapped range as `DD.MM.YYYY - DD.MM.YYYY`.
|
||||
// • Days bleeding into the previous / next month are clickable too
|
||||
// (PrimeReact `selectOtherMonths`).
|
||||
//
|
||||
// Today (per the harness) is 2026-04-23 (Thursday). Mon-Sun of that
|
||||
// week is 2026-04-20 .. 2026-04-26. Clicking 29 April (Wednesday of
|
||||
// the next calendar week) must yield 2026-04-27 .. 2026-05-03.
|
||||
|
||||
test.describe("Schedule date-range picker (week-snap)", () => {
|
||||
test("single click snaps to Mon-Sun, closes panel, fills input", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/ru-ru/schedule");
|
||||
await expect(page.getByTestId("date-range-input")).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Open the calendar via its trigger button.
|
||||
await page.locator("button.p-datepicker-trigger").first().click();
|
||||
const panel = page.locator(".p-datepicker-panel, .p-datepicker").first();
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// Click 29 April (Wednesday of the 27 Apr–3 May week).
|
||||
await panel.locator('td[aria-label="29.04.2026"] span').click();
|
||||
|
||||
// Panel auto-dismissed.
|
||||
await expect(panel).toBeHidden({ timeout: 5000 });
|
||||
|
||||
// Input now holds the full week range.
|
||||
const input = page.locator("#schedule-date-from");
|
||||
await expect(input).toHaveValue("27.04.2026 - 03.05.2026");
|
||||
});
|
||||
|
||||
test("clicking a next-month bleed-in day (3 May) snaps to 4-10 May", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/ru-ru/schedule");
|
||||
await expect(page.getByTestId("date-range-input")).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await page.locator("button.p-datepicker-trigger").first().click();
|
||||
const panel = page.locator(".p-datepicker-panel, .p-datepicker").first();
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// 3 May 2026 is Sunday — visible at the bottom of April as a bleed-in
|
||||
// day from the next month. Sunday belongs to the 27 Apr–3 May week,
|
||||
// so clicking it must snap to that week (not 4-10 May).
|
||||
await panel.locator('td[aria-label="03.05.2026"] span').click();
|
||||
|
||||
await expect(panel).toBeHidden({ timeout: 5000 });
|
||||
await expect(page.locator("#schedule-date-from")).toHaveValue(
|
||||
"27.04.2026 - 03.05.2026",
|
||||
);
|
||||
});
|
||||
|
||||
test("input renders as range placeholder when empty", async ({ page }) => {
|
||||
await page.goto("/ru-ru/schedule");
|
||||
const input = page.locator("#schedule-date-from");
|
||||
await expect(input).toHaveAttribute(
|
||||
"placeholder",
|
||||
"ДД.ММ.ГГГГ - ДД.ММ.ГГГГ",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user