Add clear-button (X) to filter fields per TZ 4.1.9 Tables 11/12/14
- OB flight-number: X was always visible; now conditionally rendered only when the field has a value (hides when empty) - OB flight-date and route-date: add X button next to calendar icon, clears date state and hides itself when empty - Schedule outbound and return date-range calendars: same inline X pattern - CSS: .calendar-input-wrapper + .calendar-clear-btn added to both SCSS files (absolute-positioned left of the calendar icon) - CityAutocomplete: already correct (CSS show/hide via has-value class) - 21 new tests across OnlineBoardFilter, ScheduleFilter, CityAutocomplete (aria-label, visibility toggling, click-to-clear); all 640 pass
This commit is contained in:
@@ -276,6 +276,40 @@
|
|||||||
// margin-top removed: vertical rhythm now driven by .filter-content gap.
|
// margin-top removed: vertical rhythm now driven by .filter-content gap.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.p-calendar {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
// Place X to the left of the calendar icon (icon is ~38px wide)
|
||||||
|
right: 38px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: colors.$light-gray;
|
||||||
|
font-size: fonts.$font-size-xl;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: colors.$text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filter-content {
|
.filter-content {
|
||||||
// Vertical rhythm between filter rows. Angular's accordion content
|
// Vertical rhythm between filter rows. Angular's accordion content
|
||||||
// separates fields by ~$space-l (15px); the previous default
|
// separates fields by ~$space-l (15px); the previous default
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* Tests for OnlineBoardFilter — clear-button (X) behaviour per TZ §4.1.9
|
||||||
|
* Tables 11/12.
|
||||||
|
*
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Module mocks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const mockNavigate = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@modern-js/runtime/router", () => ({
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
useParams: () => ({ lang: "ru-ru" }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/i18n/provider.js", () => ({
|
||||||
|
useTranslation: () => ({ t: (key: string) => key }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/i18n/useLocale.js", () => ({
|
||||||
|
useLocale: () => ({ locale: "ru-ru", language: "ru" }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||||
|
useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/state/crossSectionNavigation.js", () => ({
|
||||||
|
setBoardFilter: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// PrimeReact Calendar is stubbed so tests don't require a DOM calendar widget.
|
||||||
|
vi.mock("primereact/calendar", () => ({
|
||||||
|
Calendar: (props: Record<string, unknown>) => {
|
||||||
|
const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
data-testid={props["data-testid"] as string}
|
||||||
|
placeholder={props["placeholder"] as string}
|
||||||
|
readOnly
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// PrimeReact Slider stub
|
||||||
|
vi.mock("primereact/slider", () => ({
|
||||||
|
Slider: () => <div data-testid="slider-stub" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// DayQuickPick — not needed for these tests
|
||||||
|
vi.mock("@/ui/calendar/DayQuickPick.js", () => ({
|
||||||
|
DayQuickPick: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||||
|
CityAutocomplete: (props: Record<string, unknown>) => (
|
||||||
|
<input
|
||||||
|
data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
|
||||||
|
defaultValue={(props["value"] as string) ?? ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function renderFlightTab() {
|
||||||
|
render(<OnlineBoardFilter initialTab="flight" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRouteTab() {
|
||||||
|
render(<OnlineBoardFilter initialTab="route" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("OnlineBoardFilter – clear-button (X) per TZ §4.1.9 Tables 11/12", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Flight-number field
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("4.1.9-R1: flight-number X is hidden when input is empty", () => {
|
||||||
|
renderFlightTab();
|
||||||
|
expect(screen.queryByTestId("flight-number-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R2: flight-number X appears when a value is typed", () => {
|
||||||
|
renderFlightTab();
|
||||||
|
fireEvent.change(screen.getByTestId("flight-number-input"), {
|
||||||
|
target: { value: "1234" },
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId("flight-number-clear")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R3: clicking flight-number X clears the input and hides X", () => {
|
||||||
|
renderFlightTab();
|
||||||
|
const input = screen.getByTestId("flight-number-input");
|
||||||
|
fireEvent.change(input, { target: { value: "1234" } });
|
||||||
|
|
||||||
|
const clearBtn = screen.queryByTestId("flight-number-clear")!;
|
||||||
|
expect(clearBtn).toBeTruthy();
|
||||||
|
fireEvent.click(clearBtn);
|
||||||
|
|
||||||
|
expect((input as HTMLInputElement).value).toBe("");
|
||||||
|
expect(screen.queryByTestId("flight-number-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R4: flight-number X has aria-label for a11y", () => {
|
||||||
|
renderFlightTab();
|
||||||
|
fireEvent.change(screen.getByTestId("flight-number-input"), {
|
||||||
|
target: { value: "999" },
|
||||||
|
});
|
||||||
|
const clearBtn = screen.queryByTestId("flight-number-clear")!;
|
||||||
|
expect(clearBtn.getAttribute("aria-label")).toBe("SHARED.A11Y-CLEAR");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R5: clicking X also clears any existing validation error", () => {
|
||||||
|
renderFlightTab();
|
||||||
|
// Submit with invalid number to trigger error
|
||||||
|
fireEvent.change(screen.getByTestId("flight-number-input"), {
|
||||||
|
target: { value: "abc" },
|
||||||
|
});
|
||||||
|
fireEvent.submit(screen.getByTestId("search-form"));
|
||||||
|
expect(screen.queryByTestId("flight-number-error")).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(screen.queryByTestId("flight-number-clear")!);
|
||||||
|
expect(screen.queryByTestId("flight-number-error")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// OB date (flight-tab Calendar)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Calendar is stubbed as a read-only input; the clear button is rendered
|
||||||
|
// conditionally based on the `flightDate` state (a Date | null). Because
|
||||||
|
// the Calendar stub doesn't fire an onChange, we seed the value via the
|
||||||
|
// `initialDate` + `initialTab` props which set flightDate during init.
|
||||||
|
|
||||||
|
it("4.1.9-R6: flight-date X is hidden when no date is selected", () => {
|
||||||
|
renderFlightTab();
|
||||||
|
expect(screen.queryByTestId("flight-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R7: flight-date X is present when an initial date is provided", () => {
|
||||||
|
render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
|
||||||
|
expect(screen.queryByTestId("flight-date-clear")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R8: clicking flight-date X clears the date and hides X", () => {
|
||||||
|
render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
|
||||||
|
const clearBtn = screen.queryByTestId("flight-date-clear")!;
|
||||||
|
expect(clearBtn).toBeTruthy();
|
||||||
|
fireEvent.click(clearBtn);
|
||||||
|
expect(screen.queryByTestId("flight-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R9: flight-date clear X has aria-label for a11y", () => {
|
||||||
|
render(<OnlineBoardFilter initialTab="flight" initialDate="20260601" />);
|
||||||
|
expect(screen.queryByTestId("flight-date-clear")!.getAttribute("aria-label")).toBe(
|
||||||
|
"SHARED.A11Y-CLEAR",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Route-tab date Calendar
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("4.1.9-R10: route-date X is hidden when no date is selected", () => {
|
||||||
|
renderRouteTab();
|
||||||
|
expect(screen.queryByTestId("route-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R11: route-date X is present when an initial date is provided", () => {
|
||||||
|
render(<OnlineBoardFilter initialTab="route" initialDate="20260601" />);
|
||||||
|
expect(screen.queryByTestId("route-date-clear")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-R12: clicking route-date X clears the date and hides X", () => {
|
||||||
|
render(<OnlineBoardFilter initialTab="route" initialDate="20260601" />);
|
||||||
|
const clearBtn = screen.queryByTestId("route-date-clear")!;
|
||||||
|
fireEvent.click(clearBtn);
|
||||||
|
expect(screen.queryByTestId("route-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -385,17 +385,20 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
|||||||
}}
|
}}
|
||||||
data-testid="flight-number-input"
|
data-testid="flight-number-input"
|
||||||
/>
|
/>
|
||||||
<button
|
{flightNumber && (
|
||||||
className="button-clear"
|
<button
|
||||||
type="button"
|
className="button-clear"
|
||||||
onClick={() => {
|
type="button"
|
||||||
setFlightNumber("");
|
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||||
setFlightNumberError(null);
|
onClick={() => {
|
||||||
}}
|
setFlightNumber("");
|
||||||
data-testid="flight-number-clear-button"
|
setFlightNumberError(null);
|
||||||
>
|
}}
|
||||||
×
|
data-testid="flight-number-clear"
|
||||||
</button>
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -408,19 +411,32 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
|||||||
locale={language}
|
locale={language}
|
||||||
onChange={setFlightDate}
|
onChange={setFlightDate}
|
||||||
/>
|
/>
|
||||||
<Calendar
|
<div className="calendar-input-wrapper">
|
||||||
value={flightDate}
|
<Calendar
|
||||||
onChange={(e) => setFlightDate(e.value as Date | null)}
|
value={flightDate}
|
||||||
minDate={boardMinDate}
|
onChange={(e) => setFlightDate(e.value as Date | null)}
|
||||||
maxDate={boardMaxDate}
|
minDate={boardMinDate}
|
||||||
dateFormat="dd.mm.yy"
|
maxDate={boardMaxDate}
|
||||||
placeholder={t("SHARED.DATE_FORMAT")}
|
dateFormat="dd.mm.yy"
|
||||||
showIcon
|
placeholder={t("SHARED.DATE_FORMAT")}
|
||||||
className="input--filter"
|
showIcon
|
||||||
data-testid="date-input"
|
className="input--filter"
|
||||||
inputId="search-date-input"
|
data-testid="date-input"
|
||||||
inputRef={flightDateInputRef}
|
inputId="search-date-input"
|
||||||
/>
|
inputRef={flightDateInputRef}
|
||||||
|
/>
|
||||||
|
{flightDate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="calendar-clear-btn"
|
||||||
|
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||||
|
data-testid="flight-date-clear"
|
||||||
|
onClick={() => setFlightDate(null)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -532,19 +548,32 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
|||||||
locale={language}
|
locale={language}
|
||||||
onChange={setRouteDate}
|
onChange={setRouteDate}
|
||||||
/>
|
/>
|
||||||
<Calendar
|
<div className="calendar-input-wrapper">
|
||||||
value={routeDate}
|
<Calendar
|
||||||
onChange={(e) => setRouteDate(e.value as Date | null)}
|
value={routeDate}
|
||||||
minDate={boardMinDate}
|
onChange={(e) => setRouteDate(e.value as Date | null)}
|
||||||
maxDate={boardMaxDate}
|
minDate={boardMinDate}
|
||||||
dateFormat="dd.mm.yy"
|
maxDate={boardMaxDate}
|
||||||
placeholder={t("SHARED.DATE_FORMAT")}
|
dateFormat="dd.mm.yy"
|
||||||
showIcon
|
placeholder={t("SHARED.DATE_FORMAT")}
|
||||||
className="input--filter"
|
showIcon
|
||||||
data-testid="date-input"
|
className="input--filter"
|
||||||
inputId="route-date-input"
|
data-testid="date-input"
|
||||||
inputRef={routeDateInputRef}
|
inputId="route-date-input"
|
||||||
/>
|
inputRef={routeDateInputRef}
|
||||||
|
/>
|
||||||
|
{routeDate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="calendar-clear-btn"
|
||||||
|
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||||
|
data-testid="route-date-clear"
|
||||||
|
onClick={() => setRouteDate(null)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,40 @@
|
|||||||
font-weight: fonts.$font-medium;
|
font-weight: fonts.$font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.p-calendar {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
// Place X to the left of the calendar icon (icon is ~38px wide)
|
||||||
|
right: 38px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: colors.$light-gray;
|
||||||
|
font-size: fonts.$font-size-xl;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: colors.$text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filter-button {
|
.filter-button {
|
||||||
margin-top: vars.$space-l;
|
margin-top: vars.$space-l;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* Tests for ScheduleFilter — clear-button (X) behaviour per TZ §4.1.9
|
||||||
|
* Tables 13/14.
|
||||||
|
*
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import { ScheduleFilter } from "./ScheduleFilter.js";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Module mocks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const mockNavigate = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@modern-js/runtime/router", () => ({
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
useParams: () => ({ lang: "ru-ru" }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/i18n/provider.js", () => ({
|
||||||
|
useTranslation: () => ({ t: (key: string) => key }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/i18n/useLocale.js", () => ({
|
||||||
|
useLocale: () => ({ locale: "ru-ru", language: "ru" }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||||
|
useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// PrimeReact Calendar stub — read-only input so state is driven by props
|
||||||
|
vi.mock("primereact/calendar", () => ({
|
||||||
|
Calendar: (props: Record<string, unknown>) => {
|
||||||
|
const inputRef = props["inputRef"] as React.RefObject<HTMLInputElement> | undefined;
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
data-testid={props["data-testid"] as string}
|
||||||
|
placeholder={props["placeholder"] as string}
|
||||||
|
readOnly
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// PrimeReact Slider stub
|
||||||
|
vi.mock("primereact/slider", () => ({
|
||||||
|
Slider: () => <div data-testid="slider-stub" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||||
|
CityAutocomplete: (props: Record<string, unknown>) => (
|
||||||
|
<input
|
||||||
|
data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
|
||||||
|
defaultValue={(props["value"] as string) ?? ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/dateWindow.js", () => ({
|
||||||
|
scheduleWindowBounds: () => {
|
||||||
|
const min = new Date(2026, 0, 1);
|
||||||
|
const max = new Date(2026, 11, 31);
|
||||||
|
return [min, max];
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Outbound date range
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("4.1.9-S1: schedule date-range X is hidden when no dates are selected", () => {
|
||||||
|
render(<ScheduleFilter />);
|
||||||
|
expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-S2: schedule date-range X is present when both dates are provided", () => {
|
||||||
|
render(
|
||||||
|
<ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
|
||||||
|
);
|
||||||
|
expect(screen.queryByTestId("schedule-date-clear")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-S3: clicking schedule date-range X clears the range and hides X", () => {
|
||||||
|
render(
|
||||||
|
<ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
|
||||||
|
);
|
||||||
|
const clearBtn = screen.queryByTestId("schedule-date-clear")!;
|
||||||
|
fireEvent.click(clearBtn);
|
||||||
|
expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-S4: schedule date-range clear X has aria-label for a11y", () => {
|
||||||
|
render(
|
||||||
|
<ScheduleFilter initialDateFrom="20260601" initialDateTo="20260607" />,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("schedule-date-clear")!.getAttribute("aria-label"),
|
||||||
|
).toBe("SHARED.A11Y-CLEAR");
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Return date range (only visible when `initialReturnFlights` = true)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("4.1.9-S5: return date-range X is hidden when no return dates are selected", () => {
|
||||||
|
render(<ScheduleFilter initialReturnFlights={true} />);
|
||||||
|
expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-S6: return date-range X is present when return dates are provided", () => {
|
||||||
|
render(
|
||||||
|
<ScheduleFilter
|
||||||
|
initialReturnFlights={true}
|
||||||
|
initialReturnDateFrom="20260608"
|
||||||
|
initialReturnDateTo="20260614"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.queryByTestId("schedule-return-date-clear")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-S7: clicking return date-range X clears the range and hides X", () => {
|
||||||
|
render(
|
||||||
|
<ScheduleFilter
|
||||||
|
initialReturnFlights={true}
|
||||||
|
initialReturnDateFrom="20260608"
|
||||||
|
initialReturnDateTo="20260614"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const clearBtn = screen.queryByTestId("schedule-return-date-clear")!;
|
||||||
|
fireEvent.click(clearBtn);
|
||||||
|
expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-S8: return date-range clear X has aria-label for a11y", () => {
|
||||||
|
render(
|
||||||
|
<ScheduleFilter
|
||||||
|
initialReturnFlights={true}
|
||||||
|
initialReturnDateFrom="20260608"
|
||||||
|
initialReturnDateTo="20260614"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("schedule-return-date-clear")!.getAttribute("aria-label"),
|
||||||
|
).toBe("SHARED.A11Y-CLEAR");
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Sliders — no X required (TZ Table 12/14 exclusion)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("4.1.9-S9: no clear button is rendered for the departure-time slider", () => {
|
||||||
|
render(<ScheduleFilter />);
|
||||||
|
// Time selector is present
|
||||||
|
expect(screen.queryByTestId("time-selector")).toBeTruthy();
|
||||||
|
// But no slider clear button
|
||||||
|
expect(screen.queryByTestId("time-slider-clear")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -304,21 +304,34 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
|||||||
<label className="label--filter">
|
<label className="label--filter">
|
||||||
{t("SHARED.SCHEDULES_DATE")}
|
{t("SHARED.SCHEDULES_DATE")}
|
||||||
</label>
|
</label>
|
||||||
<Calendar
|
<div className="calendar-input-wrapper">
|
||||||
value={dateRange}
|
<Calendar
|
||||||
onChange={(e) => setDateRange((e.value as (Date | null)[]) ?? [null, null])}
|
value={dateRange}
|
||||||
selectionMode="range"
|
onChange={(e) => setDateRange((e.value as (Date | null)[]) ?? [null, null])}
|
||||||
minDate={scheduleMinDate}
|
selectionMode="range"
|
||||||
maxDate={scheduleMaxDate}
|
minDate={scheduleMinDate}
|
||||||
dateFormat="dd.mm.yy"
|
maxDate={scheduleMaxDate}
|
||||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
dateFormat="dd.mm.yy"
|
||||||
showIcon
|
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||||
className="input--filter"
|
showIcon
|
||||||
data-testid="schedule-date-input"
|
className="input--filter"
|
||||||
inputId="schedule-date-input"
|
data-testid="schedule-date-input"
|
||||||
inputRef={dateRangeInputRef}
|
inputId="schedule-date-input"
|
||||||
readOnlyInput
|
inputRef={dateRangeInputRef}
|
||||||
/>
|
readOnlyInput
|
||||||
|
/>
|
||||||
|
{(dateRange[0] || dateRange[1]) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="calendar-clear-btn"
|
||||||
|
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||||
|
data-testid="schedule-date-clear"
|
||||||
|
onClick={() => setDateRange([null, null])}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="wrapper--time-selector compact-view" data-testid="time-selector">
|
<div className="wrapper--time-selector compact-view" data-testid="time-selector">
|
||||||
@@ -371,24 +384,37 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
|||||||
<label className="label--filter">
|
<label className="label--filter">
|
||||||
{t("SHARED.RETURN_FLIGHT_DATE")}
|
{t("SHARED.RETURN_FLIGHT_DATE")}
|
||||||
</label>
|
</label>
|
||||||
<Calendar
|
<div className="calendar-input-wrapper">
|
||||||
value={returnDateRange}
|
<Calendar
|
||||||
onChange={(e) =>
|
value={returnDateRange}
|
||||||
setReturnDateRange(
|
onChange={(e) =>
|
||||||
(e.value as (Date | null)[]) ?? [null, null],
|
setReturnDateRange(
|
||||||
)
|
(e.value as (Date | null)[]) ?? [null, null],
|
||||||
}
|
)
|
||||||
selectionMode="range"
|
}
|
||||||
minDate={scheduleMinDate}
|
selectionMode="range"
|
||||||
maxDate={scheduleMaxDate}
|
minDate={scheduleMinDate}
|
||||||
dateFormat="dd.mm.yy"
|
maxDate={scheduleMaxDate}
|
||||||
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
dateFormat="dd.mm.yy"
|
||||||
showIcon
|
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
|
||||||
className="input--filter"
|
showIcon
|
||||||
data-testid="schedule-return-date-input"
|
className="input--filter"
|
||||||
inputId="schedule-return-date-input"
|
data-testid="schedule-return-date-input"
|
||||||
readOnlyInput
|
inputId="schedule-return-date-input"
|
||||||
/>
|
readOnlyInput
|
||||||
|
/>
|
||||||
|
{(returnDateRange[0] || returnDateRange[1]) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="calendar-clear-btn"
|
||||||
|
aria-label={t("SHARED.A11Y-CLEAR")}
|
||||||
|
data-testid="schedule-return-date-clear"
|
||||||
|
onClick={() => setReturnDateRange([null, null])}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="wrapper--time-selector compact-view" data-testid="return-time-selector">
|
<div className="wrapper--time-selector compact-view" data-testid="return-time-selector">
|
||||||
|
|||||||
@@ -204,6 +204,50 @@ describe("CityAutocomplete", () => {
|
|||||||
expect(onChange).not.toHaveBeenCalled();
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("4.1.9-C1: has-value CSS class is applied when value is non-empty (clear button becomes visible)", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<CityAutocomplete
|
||||||
|
label="City"
|
||||||
|
placeholder="Pick"
|
||||||
|
value="MOW"
|
||||||
|
onChange={vi.fn()}
|
||||||
|
dictionaries={dictionaries}
|
||||||
|
testIdPrefix="test"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container.querySelector(".city-autocomplete__input--has-value")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-C2: has-value CSS class is absent when value is empty (clear button hidden)", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<CityAutocomplete
|
||||||
|
label="City"
|
||||||
|
placeholder="Pick"
|
||||||
|
value=""
|
||||||
|
onChange={vi.fn()}
|
||||||
|
dictionaries={dictionaries}
|
||||||
|
testIdPrefix="test"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container.querySelector(".city-autocomplete__input--has-value")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("4.1.9-C3: clear button has aria-label for a11y", () => {
|
||||||
|
render(
|
||||||
|
<CityAutocomplete
|
||||||
|
label="City"
|
||||||
|
placeholder="Pick"
|
||||||
|
value="MOW"
|
||||||
|
onChange={vi.fn()}
|
||||||
|
dictionaries={dictionaries}
|
||||||
|
testIdPrefix="test"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const clearBtn = screen.getByTestId("test-clear-button");
|
||||||
|
// aria-label is always rendered; visibility is controlled via CSS class
|
||||||
|
expect(clearBtn.getAttribute("aria-label")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("does not open popup when dictionaries is null", () => {
|
it("does not open popup when dictionaries is null", () => {
|
||||||
render(
|
render(
|
||||||
<CityAutocomplete
|
<CityAutocomplete
|
||||||
|
|||||||
Reference in New Issue
Block a user