Add back button + flight schedule timeline (B.6) implementation plan

This commit is contained in:
2026-04-17 01:58:47 +03:00
parent df79213186
commit 888d19f8f3
@@ -0,0 +1,953 @@
# Back Button + Flight Schedule Timeline (B.6) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add Angular-parity navigation (back button in header) and a schedule timeline (accordion with scheduled times + duration + days-of-week strip + week date-range note) to the React Online Board details page.
**Architecture:** Two independent feature directories. `DetailsBackButton` is a simple `<Link>`. `FlightSchedule` is a PrimeReact `Accordion` wrapping a Schedule tab, plus a persistent days-of-week strip below. A small `weekDateRange` helper uses `date-fns` ISO week functions. `IFlightLeg` gains an optional `daysOfWeek` field.
**Tech Stack:** React 18, PrimeReact `Accordion` + `AccordionTab`, `date-fns` (`startOfISOWeek`, `endOfISOWeek`, `parseISO`, `format`), React Router `<Link>`, Vitest + React Testing Library.
---
## File Structure
### New files
- `src/features/online-board/components/DetailsBackButton/DetailsBackButton.tsx`
- `src/features/online-board/components/DetailsBackButton/DetailsBackButton.scss`
- `src/features/online-board/components/DetailsBackButton/DetailsBackButton.test.tsx`
- `src/features/online-board/components/DetailsBackButton/index.ts`
- `src/features/online-board/components/FlightSchedule/weekDateRange.ts`
- `src/features/online-board/components/FlightSchedule/weekDateRange.test.ts`
- `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.tsx`
- `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.test.tsx`
- `src/features/online-board/components/FlightSchedule/FlightSchedule.tsx`
- `src/features/online-board/components/FlightSchedule/FlightSchedule.scss`
- `src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx`
- `src/features/online-board/components/FlightSchedule/index.ts`
### Modified files
- `src/features/online-board/types.ts` — add `IDaysOfWeek`, extend `IFlightLeg` with optional `daysOfWeek`
- `src/features/online-board/types.test.ts` — add test case
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — swap PageTabs→DetailsBackButton, insert FlightSchedule
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — update mocks/assertions
---
### Task 1: Extend `IFlightLeg` with `daysOfWeek`
**Files:**
- Modify: `src/features/online-board/types.ts`
- Modify: `src/features/online-board/types.test.ts`
- [ ] **Step 1: Add failing test**
Append to `src/features/online-board/types.test.ts` inside the existing `describe("online-board types extension")` block:
```typescript
it("IDaysOfWeek has current and flight bit strings", () => {
const d: IDaysOfWeek = { current: "1000010", flight: "1111111" };
expect(d.current).toBe("1000010");
expect(d.flight).toBe("1111111");
});
it("IFlightLeg accepts optional daysOfWeek", () => {
const leg: Partial<IFlightLeg> = {
daysOfWeek: { current: "1000010", flight: "1111111" },
};
expect(leg.daysOfWeek?.flight).toBe("1111111");
});
```
Also update the imports at the top of `types.test.ts` to include `IDaysOfWeek`:
Find the existing `import type {` block and add `IDaysOfWeek,` to the list.
- [ ] **Step 2: Run test to verify fail**
Run: `pnpm vitest run src/features/online-board/types.test.ts`
Expected: FAIL — `IDaysOfWeek` is not exported.
- [ ] **Step 3: Extend `src/features/online-board/types.ts`**
Add a new exported interface near the `IFlightLegFlags` (just before the `IFlightLeg` definition):
```typescript
// ---------------------------------------------------------------------------
// Days of week
// ---------------------------------------------------------------------------
/**
* Bit-string indicator of flight operation days.
* Position 0 = Monday, position 6 = Sunday. `"1"` means active.
*/
export interface IDaysOfWeek {
current: string;
flight: string;
}
```
Then add `daysOfWeek?: IDaysOfWeek;` to the `IFlightLeg` interface (add at the end, preserving other fields):
```typescript
export interface IFlightLeg {
arrival: IFlightLegArrivalStation;
dayChange: number;
departure: IFlightLegDepartureStation;
equipment: { name?: string; code?: string } & IEquipmentFull;
flags: IFlightLegFlags;
flyingTime: string;
index: number;
operatingBy: { carrier?: string; flightNumber?: string };
status: FlightStatus;
updated: string;
transition?: IFlightTransitions;
daysOfWeek?: IDaysOfWeek;
}
```
- [ ] **Step 4: Run test to verify pass**
Run: `pnpm vitest run src/features/online-board/types.test.ts`
Expected: all tests pass.
- [ ] **Step 5: Commit**
```bash
git add src/features/online-board/types.ts src/features/online-board/types.test.ts
git commit -m "Extend IFlightLeg with optional daysOfWeek field"
```
---
### Task 2: `weekDateRange` Helper
**Files:**
- Create: `src/features/online-board/components/FlightSchedule/weekDateRange.ts`
- Create: `src/features/online-board/components/FlightSchedule/weekDateRange.test.ts`
- [ ] **Step 1: Write failing test**
```typescript
// weekDateRange.test.ts
import { describe, it, expect } from "vitest";
import { getWeekDateRange } from "./weekDateRange.js";
describe("getWeekDateRange", () => {
it("returns Monday/Sunday for a mid-week date", () => {
// 2026-04-15 is a Wednesday
const result = getWeekDateRange("2026-04-15T10:00:00");
expect(result.start).toBe("13.04.2026"); // Monday
expect(result.end).toBe("19.04.2026"); // Sunday
});
it("returns Monday/Sunday for a Sunday input (same ISO week)", () => {
// 2026-04-19 is a Sunday
const result = getWeekDateRange("2026-04-19T10:00:00");
expect(result.start).toBe("13.04.2026");
expect(result.end).toBe("19.04.2026");
});
it("returns Monday/Sunday for a Monday input", () => {
const result = getWeekDateRange("2026-04-13T10:00:00");
expect(result.start).toBe("13.04.2026");
expect(result.end).toBe("19.04.2026");
});
it("returns empty strings for invalid input", () => {
const result = getWeekDateRange("not-a-date");
expect(result).toEqual({ start: "", end: "" });
});
it("returns empty strings for undefined input", () => {
const result = getWeekDateRange(undefined);
expect(result).toEqual({ start: "", end: "" });
});
});
```
- [ ] **Step 2: Verify fail**
Run: `pnpm vitest run src/features/online-board/components/FlightSchedule/weekDateRange.test.ts`
Expected: FAIL.
- [ ] **Step 3: Create `weekDateRange.ts`**
```typescript
import { parseISO, startOfISOWeek, endOfISOWeek, format, isValid } from "date-fns";
/**
* Given a scheduled local-time ISO string, return the ISO-week bounds
* (Monday/Sunday) as `dd.MM.yyyy` formatted strings.
* Returns `{ start: "", end: "" }` for invalid or missing input.
*/
export function getWeekDateRange(scheduledLocal: string | undefined): {
start: string;
end: string;
} {
if (!scheduledLocal) return { start: "", end: "" };
const d = parseISO(scheduledLocal);
if (!isValid(d)) return { start: "", end: "" };
return {
start: format(startOfISOWeek(d), "dd.MM.yyyy"),
end: format(endOfISOWeek(d), "dd.MM.yyyy"),
};
}
```
- [ ] **Step 4: Verify pass**
Run: `pnpm vitest run src/features/online-board/components/FlightSchedule/weekDateRange.test.ts`
Expected: 5 tests pass.
- [ ] **Step 5: Commit**
```bash
git add src/features/online-board/components/FlightSchedule/weekDateRange.ts src/features/online-board/components/FlightSchedule/weekDateRange.test.ts
git commit -m "Add weekDateRange helper for flight schedule note"
```
---
### Task 3: `DaysOfWeekStrip` Component
**Files:**
- Create: `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.tsx`
- Create: `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.test.tsx`
- [ ] **Step 1: Write failing test**
```tsx
// @vitest-environment jsdom
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { DaysOfWeekStrip } from "./DaysOfWeekStrip.js";
vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({ t: (k: string) => k }),
}));
describe("DaysOfWeekStrip", () => {
it("renders 7 day boxes", () => {
render(<DaysOfWeekStrip flightBitString="1111111" />);
const boxes = screen.getAllByTestId(/^day-of-week-\d$/);
expect(boxes).toHaveLength(7);
});
it("marks first 3 active, last 4 inactive for '1110000'", () => {
render(<DaysOfWeekStrip flightBitString="1110000" />);
expect(screen.getByTestId("day-of-week-0").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-1").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-2").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-3").className).toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-6").className).toMatch(/--inactive/);
});
it("marks all active for '1111111'", () => {
render(<DaysOfWeekStrip flightBitString="1111111" />);
for (let i = 0; i < 7; i++) {
expect(screen.getByTestId(`day-of-week-${i}`).className).not.toMatch(/--inactive/);
}
});
it("marks all inactive for '0000000'", () => {
render(<DaysOfWeekStrip flightBitString="0000000" />);
for (let i = 0; i < 7; i++) {
expect(screen.getByTestId(`day-of-week-${i}`).className).toMatch(/--inactive/);
}
});
it("renders DAYS.1 through DAYS.7 labels in order", () => {
render(<DaysOfWeekStrip flightBitString="1111111" />);
expect(screen.getByTestId("day-of-week-0").textContent).toContain("DAYS.1");
expect(screen.getByTestId("day-of-week-6").textContent).toContain("DAYS.7");
});
});
```
- [ ] **Step 2: Verify fail**
Run: `pnpm vitest run src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.test.tsx`
Expected: FAIL.
- [ ] **Step 3: Create `DaysOfWeekStrip.tsx`**
```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
export interface DaysOfWeekStripProps {
flightBitString: string;
}
const DAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const;
export const DaysOfWeekStrip: FC<DaysOfWeekStripProps> = ({ flightBitString }) => {
const { t } = useTranslation();
return (
<div className="days-of-week-strip">
{DAY_INDEXES.map((i) => {
const isActive = flightBitString[i] === "1";
const className = isActive ? "day" : "day day--inactive";
return (
<span
key={i}
className={className}
data-testid={`day-of-week-${i}`}
>
{t(`DAYS.${i + 1}`)}
</span>
);
})}
</div>
);
};
```
- [ ] **Step 4: Verify pass**
Run: `pnpm vitest run src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.test.tsx`
Expected: 5 tests pass.
- [ ] **Step 5: Commit**
```bash
git add src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.tsx src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.test.tsx
git commit -m "Add DaysOfWeekStrip component for flight schedule"
```
---
### Task 4: `FlightSchedule` Component + SCSS + Barrel
**Files:**
- Create: `src/features/online-board/components/FlightSchedule/FlightSchedule.tsx`
- Create: `src/features/online-board/components/FlightSchedule/FlightSchedule.scss`
- Create: `src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx`
- Create: `src/features/online-board/components/FlightSchedule/index.ts`
- [ ] **Step 1: Write failing test**
```tsx
// @vitest-environment jsdom
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { FlightSchedule } from "./FlightSchedule.js";
import type { ISimpleFlight } from "../../types.js";
vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({
t: (k: string) => {
if (k === "SHARED.NOTE-TIME-SCHEDULE") {
return "Valid from {START_DATE} to {END_DATE}";
}
return k;
},
}),
}));
function makeFlight(overrides: { daysOfWeek?: { current: string; flight: string } }): ISimpleFlight {
return {
id: "X", routeType: "Direct", flyingTime: "2h 30m", status: "Scheduled",
flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260415" },
operatingBy: { carrier: "SU", flightNumber: "0022" },
leg: {
arrival: {
scheduled: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" },
latest: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" },
dispatch: "", gate: "", terminal: "",
times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "12:30", localTime: "", tzOffset: 0, utc: "" } },
},
dayChange: 0,
departure: {
scheduled: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" },
latest: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" },
dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "2026-04-15T10:00:00", localTime: "10:00", tzOffset: 0, utc: "" } },
},
equipment: {},
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
flyingTime: "2h 30m", index: 0, operatingBy: {}, status: "Scheduled", updated: "",
...overrides,
},
} as ISimpleFlight;
}
describe("FlightSchedule", () => {
it("returns null when firstLeg has no daysOfWeek", () => {
const { container } = render(<FlightSchedule flight={makeFlight({})} />);
expect(container.firstChild).toBeNull();
});
it("returns null when daysOfWeek.flight is empty string", () => {
const { container } = render(
<FlightSchedule flight={makeFlight({ daysOfWeek: { current: "0000000", flight: "" } })} />,
);
expect(container.firstChild).toBeNull();
});
it("renders with data-testid=flight-schedule when daysOfWeek present", () => {
render(
<FlightSchedule flight={makeFlight({ daysOfWeek: { current: "1000010", flight: "1111111" } })} />,
);
expect(screen.getByTestId("flight-schedule")).toBeTruthy();
});
it("renders 7 day boxes from daysOfWeek.flight", () => {
render(
<FlightSchedule flight={makeFlight({ daysOfWeek: { current: "1000010", flight: "1110000" } })} />,
);
const boxes = screen.getAllByTestId(/^day-of-week-\d$/);
expect(boxes).toHaveLength(7);
});
it("substitutes {START_DATE} and {END_DATE} into the note", () => {
render(
<FlightSchedule flight={makeFlight({ daysOfWeek: { current: "1000010", flight: "1111111" } })} />,
);
// 2026-04-15 is a Wednesday; ISO week Mon 13 - Sun 19
expect(screen.getByTestId("flight-schedule-note").textContent).toContain("13.04.2026");
expect(screen.getByTestId("flight-schedule-note").textContent).toContain("19.04.2026");
});
it("renders scheduled departure/arrival times and duration", () => {
render(
<FlightSchedule flight={makeFlight({ daysOfWeek: { current: "1000010", flight: "1111111" } })} />,
);
// Note: accordion open by default; times visible in content
// departure time: the local ISO string's time component
expect(screen.getByText("10:00")).toBeTruthy();
expect(screen.getByText("12:30")).toBeTruthy();
expect(screen.getByText("2h 30m")).toBeTruthy();
});
});
```
- [ ] **Step 2: Verify fail**
Run: `pnpm vitest run src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx`
Expected: FAIL.
- [ ] **Step 3: Create `FlightSchedule.scss`**
```scss
.flight-schedule {
background: #fff;
border-radius: 8px;
padding: 16px 24px;
margin-top: 16px;
.p-accordion-content {
padding: 12px 0;
}
&__row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__label { color: #666; }
&__value { font-weight: 500; }
&__days-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eee;
}
&__section-title {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-bottom: 8px;
}
&__note {
margin-top: 12px;
font-size: 12px;
color: #666;
font-style: italic;
}
}
.days-of-week-strip {
display: flex;
gap: 8px;
flex-wrap: wrap;
.day {
padding: 8px 12px;
border-radius: 6px;
background: #e6f1fb;
font-size: 14px;
font-weight: 500;
color: #1a3a5c;
&--inactive {
background: #f6f6f6;
color: rgba(102, 102, 102, 0.5);
}
}
}
```
- [ ] **Step 4: Create `FlightSchedule.tsx`**
```tsx
import type { FC } from "react";
import { Accordion, AccordionTab } from "primereact/accordion";
import { useTranslation } from "@/i18n/provider.js";
import type { ISimpleFlight } from "../../types.js";
import { DaysOfWeekStrip } from "./DaysOfWeekStrip.js";
import { getWeekDateRange } from "./weekDateRange.js";
import "./FlightSchedule.scss";
export interface FlightScheduleProps {
flight: ISimpleFlight;
}
function formatLocalTime(iso: string | undefined): string {
if (!iso) return "";
// If it's already "HH:mm" (no date), return as-is
if (/^\d{2}:\d{2}$/.test(iso)) return iso;
// Otherwise try to extract time part from ISO: "2026-04-15T10:00:00" → "10:00"
const match = /T(\d{2}:\d{2})/.exec(iso);
return match ? match[1]! : iso;
}
export const FlightSchedule: FC<FlightScheduleProps> = ({ flight }) => {
const { t } = useTranslation();
const firstLeg = flight.routeType === "Direct" ? flight.leg : flight.legs[0];
const lastLeg = flight.routeType === "Direct" ? flight.leg : flight.legs[flight.legs.length - 1];
if (!firstLeg?.daysOfWeek?.flight) return null;
if (!lastLeg) return null;
const depLocal = firstLeg.departure.times.scheduledDeparture.local;
const arrLocal = lastLeg.arrival.times.scheduledArrival.local;
const { start, end } = getWeekDateRange(depLocal);
const noteTemplate = t("SHARED.NOTE-TIME-SCHEDULE");
const note = noteTemplate.replace("{START_DATE}", start).replace("{END_DATE}", end);
return (
<section className="flight-schedule" data-testid="flight-schedule">
<Accordion multiple={false} activeIndex={0}>
<AccordionTab header={t("SHARED.SCHEDULE-FLIGHT")}>
<div className="flight-schedule__row">
<span className="flight-schedule__label">{t("SHARED.DEPARTURE-SCHEDULED")}</span>
<span className="flight-schedule__value">{formatLocalTime(depLocal)}</span>
</div>
<div className="flight-schedule__row">
<span className="flight-schedule__label">{t("SHARED.ARRIVAL-SCHEDULED")}</span>
<span className="flight-schedule__value">{formatLocalTime(arrLocal)}</span>
</div>
<div className="flight-schedule__row">
<span className="flight-schedule__label">{t("SHARED.PATH-TIME")}</span>
<span className="flight-schedule__value">{flight.flyingTime}</span>
</div>
</AccordionTab>
</Accordion>
<div className="flight-schedule__days-section">
<div className="flight-schedule__section-title">
{t("SHARED.DAYS-EXECUTE-FLIGHT")}
</div>
<DaysOfWeekStrip flightBitString={firstLeg.daysOfWeek.flight} />
<div className="flight-schedule__note" data-testid="flight-schedule-note">{note}</div>
</div>
</section>
);
};
```
- [ ] **Step 5: Create `index.ts`**
```typescript
export { FlightSchedule } from "./FlightSchedule.js";
export type { FlightScheduleProps } from "./FlightSchedule.js";
```
- [ ] **Step 6: Verify pass**
Run: `pnpm vitest run src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx`
Expected: 6 tests pass.
- [ ] **Step 7: Commit**
```bash
git add src/features/online-board/components/FlightSchedule/
git commit -m "Add FlightSchedule accordion with days-of-week strip"
```
---
### Task 5: `DetailsBackButton` Component
**Files:**
- Create: `src/features/online-board/components/DetailsBackButton/DetailsBackButton.tsx`
- Create: `src/features/online-board/components/DetailsBackButton/DetailsBackButton.scss`
- Create: `src/features/online-board/components/DetailsBackButton/DetailsBackButton.test.tsx`
- Create: `src/features/online-board/components/DetailsBackButton/index.ts`
- [ ] **Step 1: Write failing test**
```tsx
// @vitest-environment jsdom
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { DetailsBackButton } from "./DetailsBackButton.js";
vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({ t: (k: string) => k }),
}));
vi.mock("@modern-js/runtime/router", () => ({
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; [k: string]: unknown }) => (
<a href={to} {...props}>{children}</a>
),
}));
describe("DetailsBackButton", () => {
it("has data-testid=details-back-button", () => {
render(<DetailsBackButton locale="ru" />);
expect(screen.getByTestId("details-back-button")).toBeTruthy();
});
it("links to /{locale}/onlineboard", () => {
render(<DetailsBackButton locale="ru" />);
const a = screen.getByTestId("details-back-button") as HTMLAnchorElement;
expect(a.getAttribute("href")).toBe("/ru/onlineboard");
});
it("uses the provided locale in the link", () => {
render(<DetailsBackButton locale="en" />);
const a = screen.getByTestId("details-back-button") as HTMLAnchorElement;
expect(a.getAttribute("href")).toBe("/en/onlineboard");
});
it("renders the BACK-BOARD label", () => {
render(<DetailsBackButton locale="ru" />);
expect(screen.getByText("SHARED.BACK-BOARD")).toBeTruthy();
});
});
```
- [ ] **Step 2: Verify fail**
Run: `pnpm vitest run src/features/online-board/components/DetailsBackButton/DetailsBackButton.test.tsx`
Expected: FAIL.
- [ ] **Step 3: Create `DetailsBackButton.scss`**
```scss
.details-back-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #e3f0ff;
color: #1a3a5c;
border-radius: 6px;
text-decoration: none;
font-size: 14px;
font-family: inherit;
cursor: pointer;
&:hover {
background: #c7dff5;
}
&__arrow {
display: inline-block;
font-size: 16px;
}
}
```
- [ ] **Step 4: Create `DetailsBackButton.tsx`**
```tsx
import type { FC } from "react";
import { Link } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import "./DetailsBackButton.scss";
export interface DetailsBackButtonProps {
locale: string;
}
export const DetailsBackButton: FC<DetailsBackButtonProps> = ({ locale }) => {
const { t } = useTranslation();
return (
<Link
to={`/${locale}/onlineboard`}
className="details-back-button"
data-testid="details-back-button"
>
<span className="details-back-button__arrow" aria-hidden="true">{"\u2190"}</span>
<span>{t("SHARED.BACK-BOARD")}</span>
</Link>
);
};
```
- [ ] **Step 5: Create `index.ts`**
```typescript
export { DetailsBackButton } from "./DetailsBackButton.js";
export type { DetailsBackButtonProps } from "./DetailsBackButton.js";
```
- [ ] **Step 6: Verify pass**
Run: `pnpm vitest run src/features/online-board/components/DetailsBackButton/DetailsBackButton.test.tsx`
Expected: 4 tests pass.
- [ ] **Step 7: Commit**
```bash
git add src/features/online-board/components/DetailsBackButton/
git commit -m "Add DetailsBackButton component for header navigation"
```
---
### Task 6: Wire Both into `OnlineBoardDetailsPage`
**Files:**
- Modify: `src/features/online-board/components/OnlineBoardDetailsPage.tsx`
- Modify: `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`
- [ ] **Step 1: Add integration tests**
Append to the outer describe block in `OnlineBoardDetailsPage.test.tsx`:
```tsx
describe("back button integration", () => {
it("renders DetailsBackButton in headerLeft", () => {
mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20260416"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.getByTestId("details-back-button")).toBeTruthy();
});
});
describe("flight schedule integration", () => {
it("renders FlightSchedule when firstLeg.daysOfWeek is present", () => {
const flightWithDaysOfWeek = {
...mockFlight,
leg: {
...mockFlight.leg,
daysOfWeek: { current: "1000010", flight: "1111111" },
},
};
mockState = { flight: flightWithDaysOfWeek, allFlights: [flightWithDaysOfWeek], daysOfFlight: ["20260416"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.getByTestId("flight-schedule")).toBeTruthy();
});
it("does not render FlightSchedule when daysOfWeek is absent", () => {
mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20260416"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.queryByTestId("flight-schedule")).toBeNull();
});
});
```
- [ ] **Step 2: Run tests — expect failure**
Run: `pnpm vitest run src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`
Expected: the 3 new tests fail (testids not present).
- [ ] **Step 3: Update imports in `OnlineBoardDetailsPage.tsx`**
Add near other component imports:
```tsx
import { DetailsBackButton } from "./DetailsBackButton/index.js";
import { FlightSchedule } from "./FlightSchedule/index.js";
```
- [ ] **Step 4: Swap PageTabs for DetailsBackButton**
In `OnlineBoardDetailsPage.tsx`, find `const commonLayoutProps` (around line 194):
```tsx
const commonLayoutProps = {
headerLeft: <PageTabs viewType="onlineboard" />,
breadcrumbs: [
{ label: t("BREADCRUMBS.ONLINEBOARD"), url: onlineboardHref },
],
};
```
Change to:
```tsx
const commonLayoutProps = {
headerLeft: <DetailsBackButton locale={locale} />,
breadcrumbs: [
{ label: t("BREADCRUMBS.ONLINEBOARD"), url: onlineboardHref },
],
};
```
Also find the main happy-path return's `<PageLayout>` opening:
```tsx
<PageLayout
headerLeft={<PageTabs viewType="onlineboard" />}
title={<h1 className="flight-details__flight-number">{flightNumber}</h1>}
```
Change to:
```tsx
<PageLayout
headerLeft={<DetailsBackButton locale={locale} />}
title={<h1 className="flight-details__flight-number">{flightNumber}</h1>}
```
The `PageTabs` import becomes unused — remove it from the import list at the top of the file.
- [ ] **Step 5: Insert `<FlightSchedule />` after the flight legs**
In the main return, find the closing tag after the flying-time div (after `<FlightLegs legs={legs} />` + the flying-time div, just before closing `</div>` of `.flight-details`):
```tsx
{/* Flying time */}
<div className="flight-details__flying-time" data-testid="flying-time">
Total flying time: {displayFlight.flyingTime}
</div>
</div>
```
Insert `<FlightSchedule flight={displayFlight} />` after the flying-time div (before the `</div>` closing `.flight-details`):
```tsx
{/* Flying time */}
<div className="flight-details__flying-time" data-testid="flying-time">
Total flying time: {displayFlight.flyingTime}
</div>
<FlightSchedule flight={displayFlight} />
</div>
```
- [ ] **Step 6: Run tests — expect pass**
Run: `pnpm vitest run src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`
Expected: all tests pass, including the 3 new integration tests.
- [ ] **Step 7: Full suite + typecheck**
Run: `pnpm test`
Run: `pnpm typecheck`
Expected: no new failures, no new type errors. If any integration tests elsewhere (e.g., `tests/integration/online-board/*.test.tsx`) reference `PageTabs` via the details page, they may need the `DetailsBackButton` testid instead of the `onlineboard-tab` testid — fix those test assertions minimally.
- [ ] **Step 8: Commit**
```bash
git add src/features/online-board/components/OnlineBoardDetailsPage.tsx src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
git commit -m "Wire DetailsBackButton and FlightSchedule into OnlineBoardDetailsPage"
```
---
### Task 7: Manual Browser Verification
**Files:** None — observational.
- [ ] **Step 1: Ensure dev:full is running**
Run `pnpm dev:full`. Verify `/api/appSettings` returns 200.
- [ ] **Step 2: Run the verification script**
```bash
cat << 'SCRIPT' | npx tsx --input-type=module -
import { chromium } from "@playwright/test";
import { mockAngularAPIs } from "./tests/e2e-angular/support/angular-api-mock.js";
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
await mockAngularAPIs(page);
const depUtc = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
await page.route("**/onlineboard/details*", (route) => {
route.fulfill({
status: 200, contentType: "application/json",
body: JSON.stringify({
data: {
partners: [],
routes: [{
id: "SU0022-X", routeType: "Direct", flyingTime: "1h 30m", status: "Scheduled",
flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260418", dateLT: "20260418" },
operatingBy: { carrier: "SU", flightNumber: "0022" },
leg: {
index: 0, flyingTime: "1h 30m", status: "Scheduled", updated: "2026-04-17T10:00:00Z", dayChange: 0,
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
operatingBy: { carrier: "SU", flightNumber: "0022" },
daysOfWeek: { current: "1000010", flight: "1111100" },
departure: {
scheduled: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" },
latest: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" },
dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "",
times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "2026-04-18T10:00:00", localTime: "10:00", tzOffset: 3, utc: depUtc } },
},
arrival: {
scheduled: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" },
latest: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" },
dispatch: "", gate: "", terminal: "",
times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "2026-04-18T12:30:00", localTime: "12:30", tzOffset: 3, utc: "" } },
},
equipment: {},
},
}],
daysOfFlight: ["20260418"],
},
}),
});
});
await page.goto("http://localhost:8080/ru/onlineboard/SU0022-20260418", { waitUntil: "networkidle" });
await page.waitForTimeout(3000);
const backBtn = await page.locator('[data-testid="details-back-button"]').count();
const schedule = await page.locator('[data-testid="flight-schedule"]').count();
const dayBoxes = await page.locator('[data-testid^="day-of-week-"]').count();
const noteText = await page.locator('[data-testid="flight-schedule-note"]').textContent().catch(() => "");
console.log("backBtn:", backBtn);
console.log("schedule:", schedule);
console.log("dayBoxes:", dayBoxes);
console.log("note:", noteText);
await page.screenshot({ path: "/tmp/b6-verify.png", fullPage: true });
await browser.close();
SCRIPT
```
Expected: `backBtn: 1, schedule: 1, dayBoxes: 7, note: "Valid from 13.04.2026 to 19.04.2026"` (or Russian equivalent depending on the real i18n key value).
- [ ] **Step 3: No commit — observational**