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 (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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user