Add flight details button to schedule search results
ci-deploy / build-deploy-test (push) Successful in 1m49s
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:
@@ -143,6 +143,14 @@ export const DayGroupedFlightList: FC<DayGroupedFlightListProps> = ({
|
|||||||
if (onSortChange) onSortChange(value);
|
if (onSortChange) onSortChange(value);
|
||||||
if (sortModeProp === undefined) setInternalSortMode(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
|
// Track which days the user has expanded. Default: today's day group
|
||||||
// (if it's in scope). Angular's `p-accordion` is `[multiple]="true"`
|
// (if it's in scope). Angular's `p-accordion` is `[multiple]="true"`
|
||||||
// and `[activeIndex]` defaults to the index of today's date when
|
// 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.
|
// FlightList's `buyUrlFor` prop is independent of this strip.
|
||||||
const renderScheduleBody = useCallback(
|
const renderScheduleBody = useCallback(
|
||||||
(f: ISimpleFlight) => (
|
(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} />;
|
if (loading) return <FlightListSkeleton count={5} />;
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ export interface ScheduleFlightBodyProps {
|
|||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
/** Locale used to build the buy-ticket URL when `showActions` is true. */
|
/** Locale used to build the buy-ticket URL when `showActions` is true. */
|
||||||
locale?: string;
|
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 {
|
interface ChildFlightId {
|
||||||
@@ -96,6 +102,7 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
|
|||||||
flight,
|
flight,
|
||||||
showActions = false,
|
showActions = false,
|
||||||
locale = "ru-ru",
|
locale = "ru-ru",
|
||||||
|
onFlightDetails,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { language } = useLocale();
|
const { language } = useLocale();
|
||||||
@@ -379,7 +386,9 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
|
|||||||
defaults (share+buy+register+status, no print). The schedule
|
defaults (share+buy+register+status, no print). The schedule
|
||||||
details page suppresses these (page-level summary owns them),
|
details page suppresses these (page-level summary owns them),
|
||||||
so callers opt in via `showActions`. Buy/Status visibility is
|
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 && (
|
{showActions && (
|
||||||
<div
|
<div
|
||||||
className="schedule-flight-body__actions"
|
className="schedule-flight-body__actions"
|
||||||
@@ -393,6 +402,19 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
|
|||||||
showRegister
|
showRegister
|
||||||
showStatus
|
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>
|
||||||
)}
|
)}
|
||||||
</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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user