Add flight details button to schedule search results
ci-deploy / build-deploy-test (push) Successful in 1m49s

- Add flight details button to ScheduleFlightBody component
- Button positioned after Buy button (matching Angular layout)
- Button uses SHARED.FLIGHT-DETAILS translation key
- Add onFlightDetails callback to ScheduleFlightBody props
- Add handleFlightDetails to DayGroupedFlightList
- Pass onFlightDetails to ScheduleFlightBody
- Add E2E tests for flight details button functionality
This commit is contained in:
2026-04-29 20:23:24 +03:00
parent 58e4202e99
commit f5e41a7911
3 changed files with 126 additions and 3 deletions
@@ -143,6 +143,14 @@ export const DayGroupedFlightList: FC<DayGroupedFlightListProps> = ({
if (onSortChange) onSortChange(value);
if (sortModeProp === undefined) setInternalSortMode(value);
};
const handleFlightDetails = useCallback(
(flight: ISimpleFlight) => {
if (onFlightClick) {
onFlightClick(flight);
}
},
[onFlightClick],
);
// Track which days the user has expanded. Default: today's day group
// (if it's in scope). Angular's `p-accordion` is `[multiple]="true"`
// and `[activeIndex]` defaults to the index of today's date when
@@ -252,9 +260,14 @@ export const DayGroupedFlightList: FC<DayGroupedFlightListProps> = ({
// FlightList's `buyUrlFor` prop is independent of this strip.
const renderScheduleBody = useCallback(
(f: ISimpleFlight) => (
<ScheduleFlightBody flight={f} showActions locale={locale} />
<ScheduleFlightBody
flight={f}
showActions
locale={locale}
onFlightDetails={handleFlightDetails}
/>
),
[locale],
[locale, handleFlightDetails],
);
if (loading) return <FlightListSkeleton count={5} />;
@@ -44,6 +44,12 @@ export interface ScheduleFlightBodyProps {
showActions?: boolean;
/** Locale used to build the buy-ticket URL when `showActions` is true. */
locale?: string;
/**
* Callback fired when the user clicks the flight details button.
* Mirrors Angular's `flight-details-body-actions` → `flight-actions`
* → `flight-details-button` → `toDetails` event.
*/
onFlightDetails?: (flight: ISimpleFlight) => void;
}
interface ChildFlightId {
@@ -96,6 +102,7 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
flight,
showActions = false,
locale = "ru-ru",
onFlightDetails,
}) => {
const { t } = useTranslation();
const { language } = useLocale();
@@ -379,7 +386,9 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
defaults (share+buy+register+status, no print). The schedule
details page suppresses these (page-level summary owns them),
so callers opt in via `showActions`. Buy/Status visibility is
gated by `<FlightActions>` (TZ §4.1.14.4.4 / §4.1.14.4.5). */}
gated by `<FlightActions>` (TZ §4.1.14.4.4 / §4.1.14.4.5).
The flight details button is rendered after the other actions,
matching Angular's `flight-actions` component layout. */}
{showActions && (
<div
className="schedule-flight-body__actions"
@@ -393,6 +402,19 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
showRegister
showStatus
/>
{onFlightDetails && (
<button
type="button"
className="schedule-flight-body__details-btn"
data-testid="flight-details-button"
onClick={(e) => {
e.stopPropagation();
onFlightDetails(flight);
}}
>
{t("SHARED.FLIGHT-DETAILS")}
</button>
)}
</div>
)}
</div>
@@ -0,0 +1,88 @@
import { test, expect } from "./fixtures/console-gate";
test.describe("Schedule flight details button", () => {
test("flight details button is visible in expanded flight body", async ({
page,
}) => {
await page.goto("/ru-ru/schedule/route/SVO-LED-20260415");
const cards = page.locator(".flight-card--clickable");
await expect(cards.first()).toBeVisible({ timeout: 30000 });
await cards.first().click();
const actions = page.locator('[data-testid="schedule-flight-body-actions"]');
await expect(actions).toBeVisible({ timeout: 10000 });
const detailsBtn = actions.locator('[data-testid="flight-details-button"]');
await expect(detailsBtn).toBeVisible();
});
test("flight details button has correct label (Russian)", async ({ page }) => {
await page.goto("/ru-ru/schedule/route/SVO-LED-20260415");
const cards = page.locator(".flight-card--clickable");
await expect(cards.first()).toBeVisible({ timeout: 30000 });
await cards.first().click();
const detailsBtn = page.locator('[data-testid="flight-details-button"]');
await expect(detailsBtn).toBeVisible();
const text = await detailsBtn.textContent();
expect(text).toContain("Детали");
});
test("flight details button navigates to flight details page", async ({
page,
}) => {
await page.goto("/ru-ru/schedule/route/SVO-LED-20260415");
const cards = page.locator(".flight-card--clickable");
await expect(cards.first()).toBeVisible({ timeout: 30000 });
await cards.first().click();
const detailsBtn = page.locator('[data-testid="flight-details-button"]');
await detailsBtn.click();
await expect(page).toHaveURL(/\/ru-ru\/schedule\/[A-Z]{3}\/SU\d+-\d{8}\/[A-Z]{3}/);
});
test("flight details button works for connecting flights", async ({
page,
}) => {
await page.goto("/ru-ru/schedule/route/MOW-MMK-20260427-20260503");
const cards = page.locator(".flight-card--clickable");
await expect(cards.first()).toBeVisible({ timeout: 30000 });
const firstCard = cards.first();
await firstCard.click();
const detailsBtn = page.locator('[data-testid="flight-details-button"]');
await expect(detailsBtn).toBeVisible({ timeout: 10000 });
await detailsBtn.click();
await expect(page).toHaveURL(
/\/ru-ru\/schedule\/[A-Z]{3}\/SU\d+-\d{8}\/[A-Z]{3}\/SU\d+-\d{8}\/[A-Z]{3}/,
);
});
test("flight details button preserves search context in URL", async ({
page,
}) => {
await page.goto("/ru-ru/schedule/route/SVO-LED-20260415");
const cards = page.locator(".flight-card--clickable");
await expect(cards.first()).toBeVisible({ timeout: 30000 });
await cards.first().click();
const detailsBtn = page.locator('[data-testid="flight-details-button"]');
await detailsBtn.click();
const url = page.url();
expect(url).toContain("?request=");
expect(url).toContain("schedule-route-SVO-LED-20260415");
});
});