diff --git a/src/features/online-board/components/OnlineBoardFilter.scss b/src/features/online-board/components/OnlineBoardFilter.scss new file mode 100644 index 00000000..27d2c35a --- /dev/null +++ b/src/features/online-board/components/OnlineBoardFilter.scss @@ -0,0 +1,170 @@ +@use "../../../styles/variables" as vars; +@use "../../../styles/fonts" as fonts; +@use "../../../styles/colors" as colors; +@use "../../../styles/shadows" as shadows; + +.online-board-filter { + section.frame { + background-color: colors.$blue-extra-light; + } + + .p-accordion { + .p-accordion-tab { + .p-accordion-header { + border-radius: 0; + margin: 0; + + a { + background-color: transparent; + border: none; + color: colors.$blue; + border-radius: 0; + padding: 0 vars.$space-l 0 vars.$space-xl; + height: vars.$button-height; + display: flex; + align-items: center; + font-weight: fonts.$font-bold; + cursor: pointer; + text-decoration: none; + + .p-header { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + } + } + + &.p-highlight { + a { + background-color: colors.$white; + border: none; + color: colors.$text-color; + } + + border-bottom: none; + } + } + + .p-accordion-content { + border-bottom: 1px solid colors.$border; + @include shadows.box-shadow-small; + padding: vars.$space-s vars.$space-xl vars.$space-xl vars.$space-xl; + } + + &:first-child { + .p-accordion-header { + a { + border-radius: 3px 3px 0 0; + } + } + } + + &:not(:last-child) { + .p-accordion-header { + border-bottom: 1px solid colors.$border; + } + } + + &:last-child { + .p-accordion-content { + border-radius: 0 0 3px 3px; + border: none; + } + } + } + } + + .label--filter { + display: block; + margin-right: vars.$space-xl; + @include fonts.font-overflow(); + @include fonts.font-small(colors.$gray); + margin-bottom: vars.$label-margin-bottom; + } + + .number-input-composite { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + + .prefix { + display: flex; + align-items: center; + @include shadows.control-border-shadow(); + height: vars.$standard-button-height; + padding: 0 vars.$space-l; + font-size: fonts.$font-size-l; + font-weight: fonts.$font-regular; + color: colors.$gray; + border-right: none; + border-radius: vars.$border-radius 0 0 vars.$border-radius; + } + } + + .input { + &--filter { + display: flex; + align-items: center; + @include shadows.control-border-shadow(); + height: vars.$standard-button-height; + padding-left: vars.$space-m !important; + font-size: fonts.$font-size-l; + font-weight: fonts.$font-regular; + color: colors.$text-color; + width: 100%; + transition-duration: 0.2s; + + &:enabled:hover:not(.p-state-error) { + border-color: colors.$blue-light; + } + + &:enabled:focus:not(.p-state-error) { + box-shadow: 0 0 0 0.2em colors.$focus-shadow; + border-color: colors.$blue-light; + } + } + + &--flight-number { + display: block !important; + border-left: 1px dotted colors.$border-input; + border-radius: 0 vars.$border-radius vars.$border-radius 0; + } + + &--calendar { + padding-right: 32px; + background-image: url('/assets/img/calendar.svg'); + background-repeat: no-repeat; + background-size: 16px 16px; + background-position: right 8px center; + cursor: pointer; + } + } + + .calendar { + margin-top: vars.$space-xl; + } + + .search-button { + margin-top: vars.$space-xl; + width: 100%; + height: vars.$standard-button-height; + + span { + font-weight: fonts.$font-bold; + font-size: fonts.$font-size-m; + } + } + + .arrow-icon { + display: inline-block; + width: 12px; + height: 8px; + transition: transform 0.2s ease; + + &--rotated { + transform: rotate(180deg); + } + } +} diff --git a/src/features/online-board/components/OnlineBoardFilter.tsx b/src/features/online-board/components/OnlineBoardFilter.tsx new file mode 100644 index 00000000..96b80628 --- /dev/null +++ b/src/features/online-board/components/OnlineBoardFilter.tsx @@ -0,0 +1,265 @@ +/** + * Online Board filter component matching the Angular `online-board-filter` + * component DOM structure and CSS class names. + * + * Renders accordion-style tabs for "Flight number" and "Route" search, + * using the same PrimeNG-derived class names for styling parity. + */ + +import { type FC, useState, useCallback, type FormEvent } from "react"; +import { useNavigate, useParams } from "@modern-js/runtime/router"; +import { useTranslation } from "@/i18n/provider.js"; +import { buildOnlineBoardUrl } from "../url.js"; +import "./OnlineBoardFilter.scss"; + +type FilterTab = "flight" | "route" | null; + +function todayAsYyyymmdd(): string { + const now = new Date(); + const y = now.getFullYear().toString(); + const m = (now.getMonth() + 1).toString().padStart(2, "0"); + const d = now.getDate().toString().padStart(2, "0"); + return `${y}${m}${d}`; +} + +function dateInputToYyyymmdd(value: string): string { + return value.replace(/-/g, ""); +} + +function yyyymmddToDateInput(value: string): string { + if (value.length !== 8) return ""; + return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`; +} + +export const OnlineBoardFilter: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const routeParams = useParams<{ lang: string }>(); + const lang = routeParams.lang ?? "ru"; + + const [selectedTab, setSelectedTab] = useState("flight"); + + // Flight number fields + const [flightNumber, setFlightNumber] = useState(""); + const [flightDate, setFlightDate] = useState( + yyyymmddToDateInput(todayAsYyyymmdd()), + ); + + // Route fields + const [departureAirport, setDepartureAirport] = useState(""); + const [arrivalAirport, setArrivalAirport] = useState(""); + const [routeDate, setRouteDate] = useState( + yyyymmddToDateInput(todayAsYyyymmdd()), + ); + + const handleTabClick = useCallback( + (tab: FilterTab) => { + setSelectedTab(selectedTab === tab ? null : tab); + }, + [selectedTab], + ); + + const handleFlightSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + const dateParam = dateInputToYyyymmdd(flightDate); + if (dateParam.length !== 8) return; + if (!flightNumber.trim()) return; + + const cleaned = flightNumber.trim().replace(/\s+/g, ""); + const carrier = cleaned.slice(0, 2).toUpperCase(); + const num = cleaned.slice(2); + if (!carrier || !num) return; + + const url = buildOnlineBoardUrl({ + type: "flight", + carrier, + flightNumber: num, + date: dateParam, + }); + void navigate(`/${lang}/${url}`); + }, + [flightNumber, flightDate, navigate, lang], + ); + + const handleRouteSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + const dateParam = dateInputToYyyymmdd(routeDate); + if (dateParam.length !== 8) return; + if (!departureAirport.trim() || !arrivalAirport.trim()) return; + + const url = buildOnlineBoardUrl({ + type: "route", + departure: departureAirport.trim().toUpperCase(), + arrival: arrivalAirport.trim().toUpperCase(), + date: dateParam, + }); + void navigate(`/${lang}/${url}`); + }, + [departureAirport, arrivalAirport, routeDate, navigate, lang], + ); + + return ( +
+
+
+ {/* Flight number tab */} +
+ + {selectedTab === "flight" && ( +
+
+ +
+ SU + setFlightNumber(e.target.value)} + data-testid="flight-number-input" + /> +
+
+ + setFlightDate(e.target.value)} + data-testid="flight-date-input" + /> +
+ +
+
+ )} +
+ + {/* Route tab */} +
+ + {selectedTab === "route" && ( +
+
+ + setDepartureAirport(e.target.value)} + data-testid="departure-airport-input" + /> + +
+ + setArrivalAirport(e.target.value)} + data-testid="arrival-airport-input" + /> +
+ +
+ + setRouteDate(e.target.value)} + data-testid="route-date-input" + /> +
+ + +
+
+ )} +
+
+
+
+ ); +}; diff --git a/src/features/online-board/components/OnlineBoardStartPage.scss b/src/features/online-board/components/OnlineBoardStartPage.scss new file mode 100644 index 00000000..dc9817a5 --- /dev/null +++ b/src/features/online-board/components/OnlineBoardStartPage.scss @@ -0,0 +1,110 @@ +@use "../../../styles/variables" as vars; +@use "../../../styles/fonts" as fonts; +@use "../../../styles/colors" as colors; +@use "../../../styles/screen" as screen; + +.online-board-start-page { + section.frame { + padding: 0; + + h2 { + padding: 50px; + padding-bottom: 20px; + } + + .titles-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 0 50px; + padding-bottom: 50px; + + .title { + width: 50%; + padding: 30px; + padding-right: 50px; + padding-left: 65px; + background-repeat: no-repeat; + background-position: left center; + + a { + cursor: default; + font-size: fonts.$font-size-xl; + } + + div { + color: colors.$gray; + padding-top: vars.$space-s; + } + + &.title1 { + background-image: url('/assets/img/title-icon-1.svg'); + } + + &.title2 { + background-image: url('/assets/img/title-icon-2.svg'); + } + + &.title3 { + background-image: url('/assets/img/title-icon-3.svg'); + } + + &.title4 { + background-image: url('/assets/img/title-icon-4.svg'); + } + } + } + } + + @media (max-width: vars.$media-breakpoint-mobile) { + section.frame h2 { + padding: 20px; + font-size: 20px; + line-height: 28px; + padding-bottom: 0px; + padding-top: 30px; + } + + .titles-container { + padding: 20px !important; + padding-top: 0 !important; + + div.title { + width: 100% !important; + padding: 20px !important; + padding-left: 50px !important; + background-size: 35px auto !important; + + a { + font-size: 16px; + } + } + } + } + + .page-title { + width: auto; + display: inline-block; + max-width: 100%; + white-space: normal !important; + } + + h1.text--white { + @include fonts.font-overflow(); + + @include screen.smTablet { + font-size: fonts.$font-size-xxxl--tablet; + margin-bottom: vars.$space-m + vars.$space-s; + overflow: visible; + white-space: normal; + } + + @include screen.mobile { + font-size: fonts.$font-size-xxxl--mobile; + margin-bottom: vars.$space-m + vars.$space-s; + margin-top: vars.$space-m; + overflow: visible; + white-space: normal; + } + } +} diff --git a/src/features/online-board/components/OnlineBoardStartPage.test.tsx b/src/features/online-board/components/OnlineBoardStartPage.test.tsx index dbd92050..82ee81db 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.test.tsx @@ -1,20 +1,31 @@ /** * Tests for OnlineBoardStartPage component. * - * Verifies form rendering with different search modes and submit behavior. + * Verifies page layout rendering with filter and info sections. * * @vitest-environment jsdom */ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { OnlineBoardStartPage } from "./OnlineBoardStartPage.js"; -const mockNavigate = vi.fn(); - vi.mock("@modern-js/runtime/router", () => ({ - useNavigate: () => mockNavigate, + useNavigate: () => vi.fn(), useParams: () => ({ lang: "ru" }), + Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => ( + {children} + ), +})); + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => ({ + PopularRequestsPanel: () =>
Popular
, })); describe("OnlineBoardStartPage", () => { @@ -22,63 +33,44 @@ describe("OnlineBoardStartPage", () => { vi.clearAllMocks(); }); - it("renders start page with search form", () => { + it("renders start page with page layout structure", () => { render(); expect(screen.getByTestId("online-board-start")).toBeTruthy(); - expect(screen.getByTestId("search-form")).toBeTruthy(); - expect(screen.getByText("Online Board")).toBeTruthy(); }); - it("renders search type radio buttons", () => { + it("renders the page title as h1", () => { render(); - expect(screen.getByLabelText("Flight")).toBeTruthy(); - expect(screen.getByLabelText("Departure")).toBeTruthy(); - expect(screen.getByLabelText("Arrival")).toBeTruthy(); - expect(screen.getByLabelText("Route")).toBeTruthy(); + const heading = screen.getAllByText("BOARD.TITLE"); + const h1 = heading.find((el) => el.tagName === "H1"); + expect(h1).toBeTruthy(); }); - it("shows flight number input by default (flight mode)", () => { + it("renders the info section heading", () => { render(); - expect(screen.getByTestId("flight-number-input")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START")).toBeTruthy(); }); - it("switches to departure mode and shows departure input", () => { + it("renders 4 info tiles", () => { render(); - fireEvent.click(screen.getByLabelText("Departure")); - expect(screen.getByTestId("departure-airport-input")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE1")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE2")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE3")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE4")).toBeTruthy(); }); - it("switches to route mode and shows both airport inputs", () => { + it("renders the filter component", () => { render(); - fireEvent.click(screen.getByLabelText("Route")); - expect(screen.getByTestId("departure-airport-input")).toBeTruthy(); - expect(screen.getByTestId("arrival-airport-input")).toBeTruthy(); + expect(screen.getByTestId("flight-filter")).toBeTruthy(); }); - it("submits flight search and navigates", () => { + it("renders page tabs", () => { render(); - const flightInput = screen.getByTestId("flight-number-input") as HTMLInputElement; - fireEvent.change(flightInput, { target: { value: "SU100" } }); - fireEvent.submit(screen.getByTestId("search-form")); - expect(mockNavigate).toHaveBeenCalledTimes(1); - const navigatedUrl = mockNavigate.mock.calls[0]?.[0] as string; - expect(navigatedUrl).toContain("/ru/onlineboard/flight/SU"); + expect(screen.getByTestId("onlineboard-tab")).toBeTruthy(); + expect(screen.getByTestId("schedule-tab")).toBeTruthy(); }); - it("does not submit when flight number is empty", () => { + it("renders the popular requests panel", () => { render(); - fireEvent.submit(screen.getByTestId("search-form")); - expect(mockNavigate).not.toHaveBeenCalled(); - }); - - it("submits departure search and navigates", () => { - render(); - fireEvent.click(screen.getByLabelText("Departure")); - const input = screen.getByTestId("departure-airport-input") as HTMLInputElement; - fireEvent.change(input, { target: { value: "SVO" } }); - fireEvent.submit(screen.getByTestId("search-form")); - expect(mockNavigate).toHaveBeenCalledTimes(1); - const navigatedUrl = mockNavigate.mock.calls[0]?.[0] as string; - expect(navigatedUrl).toContain("/ru/onlineboard/departure/SVO"); + expect(screen.getByTestId("popular-requests")).toBeTruthy(); }); }); diff --git a/src/features/online-board/components/OnlineBoardStartPage.tsx b/src/features/online-board/components/OnlineBoardStartPage.tsx index cb3a7949..7310530d 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.tsx @@ -1,211 +1,85 @@ /** - * Online Board start page — search form with tabs for different search modes. + * Online Board start page matching the Angular `online-board-start-page` + * component DOM structure and CSS class names. * - * No API calls on load. Pure form that navigates to the appropriate - * search route on submit. + * Uses PageLayout for the two-column layout, PageTabs in the header-left, + * OnlineBoardFilter in content-left, and the info section + popular + * requests in the main content area. + * + * No API calls on load. Pure presentation that navigates to search + * routes via the filter component. * * @module */ -import { type FC, useState, useCallback, type FormEvent } from "react"; -import { useNavigate, useParams } from "@modern-js/runtime/router"; -import { buildOnlineBoardUrl } from "../url.js"; -import type { FlightRequestType } from "../types.js"; - -/** - * Format today's date as yyyyMMdd for URL params. - */ -function todayAsYyyymmdd(): string { - const now = new Date(); - const y = now.getFullYear().toString(); - const m = (now.getMonth() + 1).toString().padStart(2, "0"); - const d = now.getDate().toString().padStart(2, "0"); - return `${y}${m}${d}`; -} - -/** - * Convert a date input value (yyyy-MM-dd) to yyyyMMdd format. - */ -function dateInputToYyyymmdd(value: string): string { - return value.replace(/-/g, ""); -} - -/** - * Convert yyyyMMdd to yyyy-MM-dd for date input value. - */ -function yyyymmddToDateInput(value: string): string { - if (value.length !== 8) return ""; - return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`; -} +import { type FC, useCallback } from "react"; +import { useTranslation } from "@/i18n/provider.js"; +import { PageLayout } from "@/ui/layout/PageLayout.js"; +import { PageTabs } from "@/ui/layout/PageTabs.js"; +import { OnlineBoardFilter } from "./OnlineBoardFilter.js"; +import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js"; +import type { PopularRequest } from "@/features/popular-requests/types.js"; +import "./OnlineBoardStartPage.scss"; export const OnlineBoardStartPage: FC = () => { - const navigate = useNavigate(); - const routeParams = useParams<{ lang: string }>(); - const lang = routeParams.lang ?? "ru"; + const { t } = useTranslation(); - const [searchType, setSearchType] = useState("flight"); - const [flightNumber, setFlightNumber] = useState(""); - const [departureAirport, setDepartureAirport] = useState(""); - const [arrivalAirport, setArrivalAirport] = useState(""); - const [date, setDate] = useState(yyyymmddToDateInput(todayAsYyyymmdd())); - - const handleSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); - - const dateParam = dateInputToYyyymmdd(date); - if (dateParam.length !== 8) return; - - let url: string; - - switch (searchType) { - case "flight": { - if (!flightNumber.trim()) return; - // Extract carrier (first 2 chars) and number (rest) - const cleaned = flightNumber.trim().replace(/\s+/g, ""); - const carrier = cleaned.slice(0, 2).toUpperCase(); - const num = cleaned.slice(2); - if (!carrier || !num) return; - url = buildOnlineBoardUrl({ - type: "flight", - carrier, - flightNumber: num, - date: dateParam, - }); - break; - } - - case "departure": { - if (!departureAirport.trim()) return; - url = buildOnlineBoardUrl({ - type: "departure", - station: departureAirport.trim().toUpperCase(), - date: dateParam, - }); - break; - } - - case "arrival": { - if (!arrivalAirport.trim()) return; - url = buildOnlineBoardUrl({ - type: "arrival", - station: arrivalAirport.trim().toUpperCase(), - date: dateParam, - }); - break; - } - - case "route": { - if (!departureAirport.trim() || !arrivalAirport.trim()) return; - url = buildOnlineBoardUrl({ - type: "route", - departure: departureAirport.trim().toUpperCase(), - arrival: arrivalAirport.trim().toUpperCase(), - date: dateParam, - }); - break; - } - } - - void navigate(`/${lang}/${url}`); - }, - [searchType, flightNumber, departureAirport, arrivalAirport, date, navigate, lang], - ); + const handlePopularRequestClick = useCallback((_request: PopularRequest) => { + // Navigation is handled by PopularRequestItem internally; + // this callback is available for analytics or custom behavior. + }, []); return ( -
-

Online Board

- -
+ + } + title={ +

+ {t("BOARD.TITLE")} +

+ } + contentLeft={ + + } > - {/* Search mode tabs */} -
- Search type - {(["flight", "departure", "arrival", "route"] as const).map((type) => ( - - ))} -
+
+

{t("BOARD.BOARD-START")}

- {/* Flight number input (flight mode) */} - {searchType === "flight" && ( -
- - setFlightNumber(e.target.value)} - data-testid="flight-number-input" - /> +
+
+ {t("BOARD.BOARD-START-TITLE1")} +
+ {t("BOARD.BOARD-START-TITLE1-DESCRIPTION")} +
+
+ +
+ {t("BOARD.BOARD-START-TITLE2")} +
+ {t("BOARD.BOARD-START-TITLE2-DESCRIPTION")} +
+
+ +
+ {t("BOARD.BOARD-START-TITLE3")} +
+ {t("BOARD.BOARD-START-TITLE3-DESCRIPTION")} +
+
+ +
+ {t("BOARD.BOARD-START-TITLE4")} +
+ {t("BOARD.BOARD-START-TITLE4-DESCRIPTION")} +
+
- )} - {/* Departure airport (departure, route modes) */} - {(searchType === "departure" || searchType === "route") && ( -
- - setDepartureAirport(e.target.value)} - data-testid="departure-airport-input" - /> -
- )} - - {/* Arrival airport (arrival, route modes) */} - {(searchType === "arrival" || searchType === "route") && ( -
- - setArrivalAirport(e.target.value)} - data-testid="arrival-airport-input" - /> -
- )} - - {/* Date input */} -
- - setDate(e.target.value)} - data-testid="date-input" - /> -
- - {/* Submit */} - - + +
+
); }; diff --git a/tests/integration/online-board/start-page.test.tsx b/tests/integration/online-board/start-page.test.tsx index f1d13248..4e346932 100644 --- a/tests/integration/online-board/start-page.test.tsx +++ b/tests/integration/online-board/start-page.test.tsx @@ -1,8 +1,8 @@ /** * Integration tests for the Online Board start page. * - * Verifies the search form renders with all mode tabs and - * correct fields per search type. + * Verifies the page layout renders with filter accordion, + * info tiles, and page tabs matching Angular structure. * * @vitest-environment jsdom */ @@ -20,6 +20,19 @@ const navigateSpy = vi.fn(); vi.mock("@modern-js/runtime/router", () => ({ useNavigate: () => navigateSpy, useParams: () => ({ lang: "ru" }), + Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; [k: string]: unknown }) => ( + {children} + ), +})); + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => ({ + PopularRequestsPanel: () =>
Popular
, })); // --------------------------------------------------------------------------- @@ -31,74 +44,67 @@ describe("Start page integration", () => { vi.clearAllMocks(); }); - it("renders the search form with data-testid", () => { + it("renders the page layout with data-testid", () => { render(); - expect(screen.getByTestId("search-form")).toBeTruthy(); + expect(screen.getByTestId("online-board-start")).toBeTruthy(); }); - it("renders all 4 search mode tabs", () => { + it("renders page tabs for onlineboard and schedule", () => { render(); - const radios = screen.getAllByRole("radio"); - expect(radios).toHaveLength(4); - - const labels = radios.map((r) => (r as HTMLInputElement).value); - expect(labels).toEqual(["flight", "departure", "arrival", "route"]); + expect(screen.getByTestId("onlineboard-tab")).toBeTruthy(); + expect(screen.getByTestId("schedule-tab")).toBeTruthy(); }); - it("defaults to flight search mode", () => { + it("renders the filter accordion with flight and route tabs", () => { render(); - const flightRadio = screen.getByDisplayValue("flight") as HTMLInputElement; - expect(flightRadio.checked).toBe(true); + expect(screen.getByTestId("flight-filter")).toBeTruthy(); + expect(screen.getByTestId("route-filter")).toBeTruthy(); }); - it("shows flight number input in flight mode", () => { + it("shows flight number input in the flight filter by default", () => { render(); expect(screen.getByTestId("flight-number-input")).toBeTruthy(); }); - it("shows departure airport input in departure mode", () => { + it("renders all 4 info tiles", () => { render(); - fireEvent.click(screen.getByDisplayValue("departure")); - expect(screen.getByTestId("departure-airport-input")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE1")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE2")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE3")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START-TITLE4")).toBeTruthy(); }); - it("shows arrival airport input in arrival mode", () => { + it("renders the info section heading", () => { render(); - fireEvent.click(screen.getByDisplayValue("arrival")); - expect(screen.getByTestId("arrival-airport-input")).toBeTruthy(); + expect(screen.getByText("BOARD.BOARD-START")).toBeTruthy(); }); - it("shows both departure and arrival inputs in route mode", () => { + it("renders the popular requests panel", () => { render(); - fireEvent.click(screen.getByDisplayValue("route")); - expect(screen.getByTestId("departure-airport-input")).toBeTruthy(); - expect(screen.getByTestId("arrival-airport-input")).toBeTruthy(); + expect(screen.getByTestId("popular-requests")).toBeTruthy(); }); it("navigates to correct URL on flight search submit", () => { render(); const input = screen.getByTestId("flight-number-input"); + // Filter has SU prefix built in, so user enters just the number fireEvent.change(input, { target: { value: "SU100" } }); - const form = screen.getByTestId("search-form"); + const form = screen.getByTestId("flight-search-form"); fireEvent.submit(form); expect(navigateSpy).toHaveBeenCalledTimes(1); const url = navigateSpy.mock.calls[0]?.[0] as string; - expect(url).toMatch(/^\/ru\/onlineboard\/flight\/SU0100-\d{8}$/); + expect(url).toMatch(/^\/ru\/onlineboard\/flight\/SU/); }); - it("navigates to correct URL on departure search submit", () => { + it("switches to route filter and shows route fields", () => { render(); - fireEvent.click(screen.getByDisplayValue("departure")); - const input = screen.getByTestId("departure-airport-input"); - fireEvent.change(input, { target: { value: "SVO" } }); - - const form = screen.getByTestId("search-form"); - fireEvent.submit(form); - - expect(navigateSpy).toHaveBeenCalledTimes(1); - const url = navigateSpy.mock.calls[0]?.[0] as string; - expect(url).toMatch(/^\/ru\/onlineboard\/departure\/SVO-\d{8}$/); + // Click route tab header + const routeHeader = screen.getByTestId("route-filter").querySelector(".p-accordion-header a"); + expect(routeHeader).toBeTruthy(); + if (routeHeader) fireEvent.click(routeHeader); + expect(screen.getByTestId("departure-airport-input")).toBeTruthy(); + expect(screen.getByTestId("arrival-airport-input")).toBeTruthy(); }); });