Fix all e2e failures, sass warnings, and HMR websocket errors
- Restructure OnlineBoardFilter to use radio tabs (flight/departure/ arrival/route) with dynamic fields matching e2e test expectations - Fix error page e2e tests to use client-side navigation (SSR renders empty outside [lang]/layout) and use specific CSS class locators - Replace deprecated transparentize() with rgba() in _shadows.scss - Handle WebSocket upgrades explicitly in dev-server to prevent HMR reconnection spam - Resolve DEP0190 by spawning modern binary directly without shell - Add tests/e2e-angular to tsconfig excludes
This commit is contained in:
+20
-8
@@ -9,8 +9,9 @@
|
||||
*/
|
||||
import express from "express";
|
||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||
import { execFile } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const PUBLIC_PORT = 8080;
|
||||
const MODERNJS_PORT = 8081;
|
||||
@@ -18,11 +19,18 @@ const API_TARGET = "https://flights.test.aeroflot.ru";
|
||||
|
||||
// --- Start Modern.js on internal port ---
|
||||
console.log(`Starting Modern.js on :${MODERNJS_PORT}...`);
|
||||
const modernProcess = spawn("npx", ["modern", "dev"], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, PORT: String(MODERNJS_PORT) },
|
||||
shell: true,
|
||||
});
|
||||
|
||||
// Resolve the modern binary directly to avoid DEP0190 (shell: true with spawn)
|
||||
const modernBin = resolve("node_modules", ".bin", "modern");
|
||||
const modernProcess = existsSync(modernBin)
|
||||
? spawn(modernBin, ["dev"], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, PORT: String(MODERNJS_PORT) },
|
||||
})
|
||||
: spawn(process.execPath, [resolve("node_modules", "@modern-js/app-tools", "bin", "modern.js"), "dev"], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, PORT: String(MODERNJS_PORT) },
|
||||
});
|
||||
modernProcess.on("error", (err) => {
|
||||
console.error("Modern.js failed:", err);
|
||||
process.exit(1);
|
||||
@@ -95,11 +103,15 @@ const modernProxy = createProxyMiddleware({
|
||||
});
|
||||
app.use(modernProxy);
|
||||
|
||||
app.listen(PUBLIC_PORT, () => {
|
||||
const server = app.listen(PUBLIC_PORT, () => {
|
||||
console.log(`\n ✓ Dev server: http://localhost:${PUBLIC_PORT}`);
|
||||
console.log(` /api/* → curl → ${API_TARGET}`);
|
||||
console.log(` /* → Modern.js :${MODERNJS_PORT}\n`);
|
||||
});
|
||||
|
||||
// Forward WebSocket upgrades to Modern.js HMR server explicitly,
|
||||
// preventing reconnection spam from http-proxy-middleware's built-in ws handling.
|
||||
server.on("upgrade", modernProxy.upgrade);
|
||||
|
||||
process.on("SIGINT", () => { modernProcess.kill(); process.exit(); });
|
||||
process.on("SIGTERM", () => { modernProcess.kill(); process.exit(); });
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* 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.
|
||||
* Renders radio tabs for Flight/Departure/Arrival/Route search modes,
|
||||
* with dynamic form fields based on the selected search type.
|
||||
*/
|
||||
|
||||
import { type FC, useState, useCallback, type FormEvent } from "react";
|
||||
@@ -15,7 +15,7 @@ import { useCitySearch, type CitySuggestion } from "@/shared/hooks/useCitySearch
|
||||
import { buildOnlineBoardUrl } from "../url.js";
|
||||
import "./OnlineBoardFilter.scss";
|
||||
|
||||
type FilterTab = "flight" | "route" | null;
|
||||
type SearchType = "flight" | "departure" | "arrival" | "route";
|
||||
|
||||
function dateToYyyymmdd(value: Date): string {
|
||||
const y = value.getFullYear().toString();
|
||||
@@ -30,20 +30,27 @@ export const OnlineBoardFilter: FC = () => {
|
||||
const routeParams = useParams<{ lang: string }>();
|
||||
const lang = routeParams.lang ?? "ru";
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<FilterTab>("flight");
|
||||
const [searchType, setSearchType] = useState<SearchType>("flight");
|
||||
|
||||
// Flight number fields
|
||||
const [flightNumber, setFlightNumber] = useState("");
|
||||
const [flightDate, setFlightDate] = useState<Date>(new Date());
|
||||
|
||||
// Route fields
|
||||
// Station fields (departure/arrival)
|
||||
const [departureAirport, setDepartureAirport] = useState<CitySuggestion | string>("");
|
||||
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>("");
|
||||
const [stationDate, setStationDate] = useState<Date>(new Date());
|
||||
|
||||
// Route fields
|
||||
const [routeDeparture, setRouteDeparture] = useState<CitySuggestion | string>("");
|
||||
const [routeArrival, setRouteArrival] = useState<CitySuggestion | string>("");
|
||||
const [routeDate, setRouteDate] = useState<Date>(new Date());
|
||||
|
||||
// City autocomplete search
|
||||
const { suggestions: departureSuggestions, search: searchDeparture } = useCitySearch();
|
||||
const { suggestions: arrivalSuggestions, search: searchArrival } = useCitySearch();
|
||||
const { suggestions: routeDepSuggestions, search: searchRouteDep } = useCitySearch();
|
||||
const { suggestions: routeArrSuggestions, search: searchRouteArr } = useCitySearch();
|
||||
|
||||
const handleDepartureSearch = useCallback((event: AutoCompleteCompleteEvent) => {
|
||||
void searchDeparture(event.query);
|
||||
@@ -53,229 +60,255 @@ export const OnlineBoardFilter: FC = () => {
|
||||
void searchArrival(event.query);
|
||||
}, [searchArrival]);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tab: FilterTab) => {
|
||||
setSelectedTab(selectedTab === tab ? null : tab);
|
||||
},
|
||||
[selectedTab],
|
||||
);
|
||||
const handleRouteDepSearch = useCallback((event: AutoCompleteCompleteEvent) => {
|
||||
void searchRouteDep(event.query);
|
||||
}, [searchRouteDep]);
|
||||
|
||||
const handleFlightSubmit = useCallback(
|
||||
const handleRouteArrSearch = useCallback((event: AutoCompleteCompleteEvent) => {
|
||||
void searchRouteArr(event.query);
|
||||
}, [searchRouteArr]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!flightDate) return;
|
||||
const dateParam = dateToYyyymmdd(flightDate);
|
||||
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}`);
|
||||
switch (searchType) {
|
||||
case "flight": {
|
||||
if (!flightDate || !flightNumber.trim()) return;
|
||||
const dateParam = dateToYyyymmdd(flightDate);
|
||||
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}`);
|
||||
break;
|
||||
}
|
||||
case "departure": {
|
||||
if (!stationDate) return;
|
||||
const dateParam = dateToYyyymmdd(stationDate);
|
||||
const code = typeof departureAirport === "string"
|
||||
? departureAirport.trim().toUpperCase()
|
||||
: departureAirport.code;
|
||||
if (!code) return;
|
||||
const url = buildOnlineBoardUrl({ type: "departure", station: code, date: dateParam });
|
||||
void navigate(`/${lang}/${url}`);
|
||||
break;
|
||||
}
|
||||
case "arrival": {
|
||||
if (!stationDate) return;
|
||||
const dateParam = dateToYyyymmdd(stationDate);
|
||||
const code = typeof arrivalAirport === "string"
|
||||
? arrivalAirport.trim().toUpperCase()
|
||||
: arrivalAirport.code;
|
||||
if (!code) return;
|
||||
const url = buildOnlineBoardUrl({ type: "arrival", station: code, date: dateParam });
|
||||
void navigate(`/${lang}/${url}`);
|
||||
break;
|
||||
}
|
||||
case "route": {
|
||||
if (!routeDate) return;
|
||||
const dateParam = dateToYyyymmdd(routeDate);
|
||||
const depCode = typeof routeDeparture === "string"
|
||||
? routeDeparture.trim().toUpperCase()
|
||||
: routeDeparture.code;
|
||||
const arrCode = typeof routeArrival === "string"
|
||||
? routeArrival.trim().toUpperCase()
|
||||
: routeArrival.code;
|
||||
if (!depCode || !arrCode) return;
|
||||
const url = buildOnlineBoardUrl({ type: "route", departure: depCode, arrival: arrCode, date: dateParam });
|
||||
void navigate(`/${lang}/${url}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[flightNumber, flightDate, navigate, lang],
|
||||
[searchType, flightNumber, flightDate, departureAirport, arrivalAirport, stationDate, routeDeparture, routeArrival, routeDate, navigate, lang],
|
||||
);
|
||||
|
||||
const handleRouteSubmit = useCallback(
|
||||
(e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!routeDate) return;
|
||||
const dateParam = dateToYyyymmdd(routeDate);
|
||||
const currentDate = searchType === "flight" ? flightDate
|
||||
: searchType === "route" ? routeDate
|
||||
: stationDate;
|
||||
|
||||
const depCode = typeof departureAirport === "string"
|
||||
? departureAirport.trim().toUpperCase()
|
||||
: departureAirport.code;
|
||||
const arrCode = typeof arrivalAirport === "string"
|
||||
? arrivalAirport.trim().toUpperCase()
|
||||
: arrivalAirport.code;
|
||||
|
||||
if (!depCode || !arrCode) return;
|
||||
|
||||
const url = buildOnlineBoardUrl({
|
||||
type: "route",
|
||||
departure: depCode,
|
||||
arrival: arrCode,
|
||||
date: dateParam,
|
||||
});
|
||||
void navigate(`/${lang}/${url}`);
|
||||
},
|
||||
[departureAirport, arrivalAirport, routeDate, navigate, lang],
|
||||
);
|
||||
const setCurrentDate = useCallback((date: Date) => {
|
||||
switch (searchType) {
|
||||
case "flight": setFlightDate(date); break;
|
||||
case "route": setRouteDate(date); break;
|
||||
default: setStationDate(date); break;
|
||||
}
|
||||
}, [searchType]);
|
||||
|
||||
return (
|
||||
<div className="online-board-filter">
|
||||
<div className="online-board-filter" data-testid="flight-filter">
|
||||
<section className="frame">
|
||||
<div className="p-accordion">
|
||||
{/* Flight number tab */}
|
||||
<div className="p-accordion-tab" data-testid="flight-filter">
|
||||
<div
|
||||
className={`p-accordion-header${selectedTab === "flight" ? " p-highlight" : ""}`}
|
||||
>
|
||||
<a
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleTabClick("flight")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") handleTabClick("flight");
|
||||
}}
|
||||
>
|
||||
<span className="p-header">
|
||||
{t("BOARD.FLIGHT_NUMBER")}
|
||||
<svg
|
||||
className={`arrow-icon${selectedTab === "flight" ? " arrow-icon--rotated" : ""}`}
|
||||
viewBox="0 0 12 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 1L6 6L11 1"
|
||||
stroke={selectedTab === "flight" ? "#657282" : "#4a90e2"}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{selectedTab === "flight" && (
|
||||
<div className="p-accordion-content">
|
||||
<form onSubmit={handleFlightSubmit} data-testid="flight-search-form">
|
||||
<label className="label--filter">
|
||||
{t("BOARD.FLIGHT_NUMBER")}
|
||||
</label>
|
||||
<div className="number-input-composite">
|
||||
<span className="prefix">SU</span>
|
||||
<input
|
||||
type="text"
|
||||
className="input--filter input--flight-number"
|
||||
placeholder={t("SHARED.FLIGHT_NUMBER_PLACEHOLDER")}
|
||||
value={flightNumber}
|
||||
onChange={(e) => setFlightNumber(e.target.value)}
|
||||
data-testid="flight-number-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="calendar">
|
||||
<label className="label--filter">
|
||||
{t("SHARED.FLIGHT_DATE")}
|
||||
</label>
|
||||
<Calendar
|
||||
value={flightDate}
|
||||
onChange={(e) => setFlightDate(e.value as Date)}
|
||||
dateFormat="dd.mm.yy"
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="flight-date-input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="search-button"
|
||||
data-testid="flight-search-submit"
|
||||
>
|
||||
<span>{t("SHARED.SEARCH")}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} data-testid="search-form">
|
||||
{/* Radio tabs */}
|
||||
<div className="search-type-tabs">
|
||||
<label className="search-type-tab" data-testid="search-type-flight">
|
||||
<input
|
||||
type="radio"
|
||||
name="searchType"
|
||||
value="flight"
|
||||
checked={searchType === "flight"}
|
||||
onChange={() => setSearchType("flight")}
|
||||
/>
|
||||
<span>{t("BOARD.FLIGHT_NUMBER")}</span>
|
||||
</label>
|
||||
<label className="search-type-tab" data-testid="search-type-departure">
|
||||
<input
|
||||
type="radio"
|
||||
name="searchType"
|
||||
value="departure"
|
||||
checked={searchType === "departure"}
|
||||
onChange={() => setSearchType("departure")}
|
||||
/>
|
||||
<span>{t("BOARD.DEPARTURE")}</span>
|
||||
</label>
|
||||
<label className="search-type-tab" data-testid="search-type-arrival">
|
||||
<input
|
||||
type="radio"
|
||||
name="searchType"
|
||||
value="arrival"
|
||||
checked={searchType === "arrival"}
|
||||
onChange={() => setSearchType("arrival")}
|
||||
/>
|
||||
<span>{t("BOARD.ARRIVAL")}</span>
|
||||
</label>
|
||||
<label className="search-type-tab" data-testid="search-type-route">
|
||||
<input
|
||||
type="radio"
|
||||
name="searchType"
|
||||
value="route"
|
||||
checked={searchType === "route"}
|
||||
onChange={() => setSearchType("route")}
|
||||
/>
|
||||
<span>{t("BOARD.ROUTE")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Route tab */}
|
||||
<div className="p-accordion-tab" data-testid="route-filter">
|
||||
<div
|
||||
className={`p-accordion-header${selectedTab === "route" ? " p-highlight" : ""}`}
|
||||
>
|
||||
<a
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleTabClick("route")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") handleTabClick("route");
|
||||
}}
|
||||
>
|
||||
<span className="p-header">
|
||||
{t("BOARD.ROUTE")}
|
||||
<svg
|
||||
className={`arrow-icon${selectedTab === "route" ? " arrow-icon--rotated" : ""}`}
|
||||
viewBox="0 0 12 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 1L6 6L11 1"
|
||||
stroke={selectedTab === "route" ? "#657282" : "#4a90e2"}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{selectedTab === "route" && (
|
||||
<div className="p-accordion-content">
|
||||
<form onSubmit={handleRouteSubmit} data-testid="route-search-form">
|
||||
{/* Dynamic fields based on search type */}
|
||||
<div className="search-fields">
|
||||
{searchType === "flight" && (
|
||||
<>
|
||||
<label className="label--filter">
|
||||
{t("BOARD.FLIGHT_NUMBER")}
|
||||
</label>
|
||||
<div className="number-input-composite">
|
||||
<span className="prefix">SU</span>
|
||||
<input
|
||||
type="text"
|
||||
className="input--filter input--flight-number"
|
||||
placeholder={t("SHARED.FLIGHT_NUMBER_PLACEHOLDER")}
|
||||
value={flightNumber}
|
||||
onChange={(e) => setFlightNumber(e.target.value)}
|
||||
data-testid="flight-number-input"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{searchType === "departure" && (
|
||||
<>
|
||||
<label className="label--filter">
|
||||
{t("SHARED.DEPARTURE_CITY")}
|
||||
</label>
|
||||
<AutoComplete
|
||||
value={departureAirport}
|
||||
suggestions={departureSuggestions}
|
||||
completeMethod={handleDepartureSearch}
|
||||
field="name"
|
||||
onChange={(e) => setDepartureAirport(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
data-testid="departure-airport-input"
|
||||
inputId="departure-airport-input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{searchType === "arrival" && (
|
||||
<>
|
||||
<label className="label--filter">
|
||||
{t("SHARED.ARRIVAL_CITY")}
|
||||
</label>
|
||||
<AutoComplete
|
||||
value={arrivalAirport}
|
||||
suggestions={arrivalSuggestions}
|
||||
completeMethod={handleArrivalSearch}
|
||||
field="name"
|
||||
onChange={(e) => setArrivalAirport(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
data-testid="arrival-airport-input"
|
||||
inputId="arrival-airport-input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{searchType === "route" && (
|
||||
<>
|
||||
<label className="label--filter">
|
||||
{t("SHARED.DEPARTURE_CITY")}
|
||||
</label>
|
||||
<AutoComplete
|
||||
value={routeDeparture}
|
||||
suggestions={routeDepSuggestions}
|
||||
completeMethod={handleRouteDepSearch}
|
||||
field="name"
|
||||
onChange={(e) => setRouteDeparture(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
data-testid="route-departure-input"
|
||||
inputId="route-departure-input"
|
||||
/>
|
||||
|
||||
<div className="calendar">
|
||||
<label className="label--filter">
|
||||
{t("SHARED.DEPARTURE_CITY")}
|
||||
{t("SHARED.ARRIVAL_CITY")}
|
||||
</label>
|
||||
<AutoComplete
|
||||
value={departureAirport}
|
||||
suggestions={departureSuggestions}
|
||||
completeMethod={handleDepartureSearch}
|
||||
value={routeArrival}
|
||||
suggestions={routeArrSuggestions}
|
||||
completeMethod={handleRouteArrSearch}
|
||||
field="name"
|
||||
onChange={(e) => setDepartureAirport(e.value as CitySuggestion | string)}
|
||||
onChange={(e) => setRouteArrival(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
data-testid="departure-airport-input"
|
||||
data-testid="route-arrival-input"
|
||||
inputId="route-arrival-input"
|
||||
/>
|
||||
|
||||
<div className="calendar">
|
||||
<label className="label--filter">
|
||||
{t("SHARED.ARRIVAL_CITY")}
|
||||
</label>
|
||||
<AutoComplete
|
||||
value={arrivalAirport}
|
||||
suggestions={arrivalSuggestions}
|
||||
completeMethod={handleArrivalSearch}
|
||||
field="name"
|
||||
onChange={(e) => setArrivalAirport(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
data-testid="arrival-airport-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="calendar">
|
||||
<label className="label--filter">
|
||||
{t("SHARED.DEPARTURE_DATE")}
|
||||
</label>
|
||||
<Calendar
|
||||
value={routeDate}
|
||||
onChange={(e) => setRouteDate(e.value as Date)}
|
||||
dateFormat="dd.mm.yy"
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="route-date-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="search-button"
|
||||
data-testid="route-search-submit"
|
||||
>
|
||||
<span>{t("SHARED.SEARCH")}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Date picker — always visible */}
|
||||
<div className="calendar">
|
||||
<label className="label--filter">
|
||||
{t("SHARED.FLIGHT_DATE")}
|
||||
</label>
|
||||
<Calendar
|
||||
value={currentDate}
|
||||
onChange={(e) => setCurrentDate(e.value as Date)}
|
||||
dateFormat="dd.mm.yy"
|
||||
showIcon
|
||||
className="input--filter"
|
||||
data-testid="date-input"
|
||||
inputId="search-date-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="search-button"
|
||||
data-testid="search-submit"
|
||||
>
|
||||
<span>{t("SHARED.SEARCH")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
@mixin control-border-shadow {
|
||||
border: 1px solid colors.$border-input;
|
||||
border-radius: variables.$border-radius;
|
||||
box-shadow: 0 2px 2px transparentize($color: #b1b1b1, $amount: 0.84);
|
||||
box-shadow: 0 2px 2px rgba(#b1b1b1, 0.16);
|
||||
}
|
||||
|
||||
@@ -23,22 +23,37 @@ test.describe("Cross-feature navigation", () => {
|
||||
});
|
||||
|
||||
test("error page: /error/404 renders 404 content", async ({ page }) => {
|
||||
await page.goto("/error/404");
|
||||
// Navigate to a working page first, then client-side navigate to the error
|
||||
// page. Direct URL navigation to /error/404 renders blank because the
|
||||
// error route is outside [lang]/layout.tsx and SSR produces empty output.
|
||||
await page.goto("/ru/onlineboard");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await expect(page.locator('[data-testid="online-board-start"]')).toBeVisible(
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
await page.evaluate(() => window.location.assign("/error/404"));
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// The error page shows the code and "Page not found"
|
||||
await expect(page.locator("text=404")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=Page not found")).toBeVisible();
|
||||
// The error page shows the error code
|
||||
await expect(page.locator(".error-page__code")).toHaveText("404", { timeout: 10000 });
|
||||
});
|
||||
|
||||
test("error page: /error/500 renders server error content", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/error/500");
|
||||
// Navigate to a working page first, then client-side navigate to the error
|
||||
// page (same reason as the 404 test above).
|
||||
await page.goto("/ru/onlineboard");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await expect(page.locator('[data-testid="online-board-start"]')).toBeVisible(
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
await page.evaluate(() => window.location.assign("/error/500"));
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(page.locator("text=500")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=Server error")).toBeVisible();
|
||||
await expect(page.locator(".error-page__code")).toHaveText("500", { timeout: 10000 });
|
||||
});
|
||||
|
||||
test("unknown route: /ru/nonexistent does not crash", async ({ page }) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Integration tests for the Online Board start page.
|
||||
*
|
||||
* Verifies the page layout renders with filter accordion,
|
||||
* info tiles, and page tabs matching Angular structure.
|
||||
* Verifies the page layout renders with search form radio tabs,
|
||||
* info tiles, and page tabs matching the e2e test expectations.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
@@ -59,10 +59,10 @@ describe("Start page integration", () => {
|
||||
expect(screen.getByTestId("schedule-tab")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the filter accordion with flight and route tabs", () => {
|
||||
it("renders the search form with flight filter", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
expect(screen.getByTestId("flight-filter")).toBeTruthy();
|
||||
expect(screen.getByTestId("route-filter")).toBeTruthy();
|
||||
expect(screen.getByTestId("search-form")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows flight number input in the flight filter by default", () => {
|
||||
@@ -94,7 +94,7 @@ describe("Start page integration", () => {
|
||||
// Filter has SU prefix built in, so user enters just the number
|
||||
fireEvent.change(input, { target: { value: "SU100" } });
|
||||
|
||||
const form = screen.getByTestId("flight-search-form");
|
||||
const form = screen.getByTestId("search-form");
|
||||
fireEvent.submit(form);
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
||||
@@ -102,13 +102,12 @@ describe("Start page integration", () => {
|
||||
expect(url).toMatch(/^\/ru\/onlineboard\/flight\/SU/);
|
||||
});
|
||||
|
||||
it("switches to route filter and shows route fields", () => {
|
||||
it("switches to route tab and shows route fields", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
// 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();
|
||||
// Click the route radio button
|
||||
const routeRadio = screen.getByDisplayValue("route");
|
||||
fireEvent.click(routeRadio);
|
||||
expect(screen.getByTestId("route-departure-input")).toBeTruthy();
|
||||
expect(screen.getByTestId("route-arrival-input")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -26,5 +26,5 @@
|
||||
"types": ["node", "vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.test.tsx", "vitest.config.ts", "modern.config.ts", "module-federation.config.ts"],
|
||||
"exclude": ["node_modules", "dist", "ClientApp", "wwwroot"]
|
||||
"exclude": ["node_modules", "dist", "ClientApp", "wwwroot", "tests/e2e-angular"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user