Audit multi-segment flight details per TZ 4.1.15.5 + 4.1.16.5

Gap found and fixed: Timeline route bar (Маршрут section) was rendering
departure/arrival times without day-change badges. TZ §4.1.15.5 rows 3
and 9 require +X/-X indicators whenever a leg crosses midnight.

Added TimeCell component to Timeline that emits the badge when
dayChange != 0, with priority to actual times when canChange=true
(Online Board) and fallback to scheduled (Schedule). Added 9 new
assertion tests covering: no badge when 0, +1/+2/-1 on arrival, badge
on departure, actual-takes-priority, and multi-badge (3 badges when 3
of 4 time cells carry non-zero day offsets).

All other multi-segment rules (routeChanged/returnToAirport from any
leg, codesharing in header, StationChange detection, TransferBar,
per-leg LegRoute with its own arrival day-change badge, ScheduleFlightBody
per-leg TimeGroup) were verified as already implemented. Per-segment
collapse/expand accordion (rows 7 of §4.1.15.5) deferred to Task 13.
This commit is contained in:
2026-04-22 00:32:06 +03:00
parent 21b6c90d0f
commit 7fcb844b82
3 changed files with 159 additions and 8 deletions
@@ -48,11 +48,26 @@
font-weight: fonts.$font-medium;
color: colors.$blue-dark;
}
&--stations {
font-size: fonts.$font-size-m;
color: colors.$light-gray;
}
}
// §4.1.15.5 Маршрут rows 3 & 9: "+X"/"-X" day-change badge on time cells.
&__time-cell {
display: inline-flex;
align-items: baseline;
gap: 2px;
}
&__day-change {
font-size: fonts.$font-size-s;
font-weight: fonts.$font-bold;
color: colors.$orange;
line-height: 1;
}
}
.timeline-section {
@@ -15,6 +15,14 @@ function makeLeg(
actualDepLocal?: string;
actualArrLocal?: string;
estimatedDuration?: IDuration;
/** day-change value on scheduledArrival */
scheduledArrDayChange?: number;
/** day-change value on scheduledDeparture */
scheduledDepDayChange?: number;
/** day-change value on actualBlockOn */
actualArrDayChange?: number;
/** day-change value on actualBlockOff */
actualDepDayChange?: number;
} = {},
): IFlightLeg {
return {
@@ -29,7 +37,7 @@ function makeLeg(
terminal: "",
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
dayChange: { value: overrides.scheduledArrDayChange ?? 0, title: "" },
local: `a${i}`,
localTime: `a${i}`,
tzOffset: 0,
@@ -37,7 +45,7 @@ function makeLeg(
},
actualBlockOn: overrides.actualArrLocal
? {
dayChange: { value: 0, title: "" },
dayChange: { value: overrides.actualArrDayChange ?? 0, title: "" },
local: overrides.actualArrLocal,
localTime: overrides.actualArrLocal,
tzOffset: 0,
@@ -58,7 +66,7 @@ function makeLeg(
checkingStatus: "",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
dayChange: { value: overrides.scheduledDepDayChange ?? 0, title: "" },
local: `d${i}`,
localTime: `d${i}`,
tzOffset: 0,
@@ -66,7 +74,7 @@ function makeLeg(
},
actualBlockOff: overrides.actualDepLocal
? {
dayChange: { value: 0, title: "" },
dayChange: { value: overrides.actualDepDayChange ?? 0, title: "" },
local: overrides.actualDepLocal,
localTime: overrides.actualDepLocal,
tzOffset: 0,
@@ -140,4 +148,88 @@ describe("Timeline", () => {
expect(screen.getByText("d1")).toBeTruthy();
expect(screen.getByText("a1")).toBeTruthy();
});
describe("§4.1.15.5 Маршрут day-change badges (+X/-X)", () => {
it("4.1.15.5-DC-None: does not render day-change badge when value is 0", () => {
const legs = [
makeLeg(0, { scheduledArrDayChange: 0 }),
makeLeg(1, { scheduledDepDayChange: 0 }),
];
render(<Timeline legs={legs} canChange={false} />);
expect(screen.queryByTestId("timeline-day-change")).toBeNull();
});
it("4.1.15.5-DC-ScheduledArr+1: renders +1 badge on arrival time when scheduledArrival.dayChange=+1", () => {
const legs = [
makeLeg(0, { scheduledArrDayChange: 1 }),
makeLeg(1),
];
render(<Timeline legs={legs} canChange={false} />);
expect(screen.getByTestId("timeline-day-change").textContent).toBe("+1");
});
it("4.1.15.5-DC-ScheduledArr+2: renders +2 badge when dayChange value is 2", () => {
const legs = [
makeLeg(0, { scheduledArrDayChange: 2 }),
makeLeg(1),
];
render(<Timeline legs={legs} canChange={false} />);
expect(screen.getByTestId("timeline-day-change").textContent).toBe("+2");
});
it("4.1.15.5-DC-ScheduledArr-1: renders -1 badge when dayChange value is -1", () => {
const legs = [
makeLeg(0, { scheduledArrDayChange: -1 }),
makeLeg(1),
];
render(<Timeline legs={legs} canChange={false} />);
expect(screen.getByTestId("timeline-day-change").textContent).toBe("-1");
});
it("4.1.15.5-DC-PreferActual: uses actualBlockOn dayChange when canChange=true and actual present", () => {
const legs = [
// scheduledArrDayChange=0, but actual has +1 — badge should show +1
makeLeg(0, {
actualArrLocal: "ax0",
actualArrDayChange: 1,
scheduledArrDayChange: 0,
}),
makeLeg(1),
];
render(<Timeline legs={legs} canChange={true} />);
expect(screen.getByTestId("timeline-day-change").textContent).toBe("+1");
});
it("4.1.15.5-DC-FallbackScheduled: falls back to scheduled dayChange when canChange=true but no actual", () => {
const legs = [
makeLeg(0, { scheduledArrDayChange: 1 }),
makeLeg(1),
];
render(<Timeline legs={legs} canChange={true} />);
expect(screen.getByTestId("timeline-day-change").textContent).toBe("+1");
});
it("4.1.15.5-DC-DepBadge: renders day-change badge on departure time (not just arrival)", () => {
const legs = [
makeLeg(0),
// departure of leg 1 has +1 day change (flights that depart next day)
makeLeg(1, { scheduledDepDayChange: 1 }),
];
render(<Timeline legs={legs} canChange={false} />);
// At least one badge should be present
expect(screen.getByTestId("timeline-day-change").textContent).toBe("+1");
});
it("4.1.15.5-DC-MultipleBadges: renders multiple badges when several times cross midnight", () => {
const legs = [
// Leg 0 arrives +1; leg 1 departs +1 and arrives +2
makeLeg(0, { scheduledArrDayChange: 1 }),
makeLeg(1, { scheduledDepDayChange: 1, scheduledArrDayChange: 2 }),
];
render(<Timeline legs={legs} canChange={false} />);
const badges = screen.getAllByTestId("timeline-day-change");
// We render dep0, arr0, dep1, arr1 — three of them have non-zero day-change
expect(badges.length).toBe(3);
});
});
});
@@ -21,10 +21,54 @@ function arrTime(leg: IFlightLeg, canChange: boolean): string {
return t.scheduledArrival.local;
}
/**
* Extract the day-change value (+X/-X) for a departure time.
* Prefers actual if canChange and actual exists, otherwise falls back to scheduled.
* §4.1.15.5 Маршрут rows 3 & 9: "+X"/"-X" badge required when value !== 0.
*/
function depDayChange(leg: IFlightLeg, canChange: boolean): number {
const t = leg.departure.times;
if (canChange && t.actualBlockOff) return t.actualBlockOff.dayChange?.value ?? 0;
return t.scheduledDeparture.dayChange?.value ?? 0;
}
/**
* Extract the day-change value (+X/-X) for an arrival time.
* Prefers actual if canChange and actual exists, otherwise falls back to scheduled.
* §4.1.15.5 Маршрут rows 3 & 9: "+X"/"-X" badge required when value !== 0.
*/
function arrDayChange(leg: IFlightLeg, canChange: boolean): number {
const t = leg.arrival.times;
if (canChange && t.actualBlockOn) return t.actualBlockOn.dayChange?.value ?? 0;
return t.scheduledArrival.dayChange?.value ?? 0;
}
function isSpecifying(leg: IFlightLeg, canChange: boolean): boolean {
return canChange && leg.estimatedDuration?.isNegative === true;
}
/** Single time cell with optional day-change badge (+X / -X). */
interface TimeCellProps {
time: string;
/** Day offset relative to departure date: 0 = no badge, +1 = "+1", -1 = "-1" etc. */
dayChange: number;
}
const TimeCell: FC<TimeCellProps> = ({ time, dayChange }) => (
<span className="timeline__time-cell">
{time}
{dayChange !== 0 && (
<span
className="timeline__day-change"
data-testid="timeline-day-change"
aria-label={`${dayChange > 0 ? "+" : ""}${dayChange}`}
>
{dayChange > 0 ? `+${dayChange}` : `${dayChange}`}
</span>
)}
</span>
);
interface SectionProps {
legNumber: number;
duration: string;
@@ -71,20 +115,20 @@ export const Timeline: FC<TimelineProps> = ({ legs, canChange }) => {
<div className="timeline__content">
<div className="timeline__row timeline__row--times">
<span>{depTime(currentLeg, canChange)}</span>
<TimeCell time={depTime(currentLeg, canChange)} dayChange={depDayChange(currentLeg, canChange)} />
<Section
legNumber={index + 1}
duration={currentLeg.flyingTime}
specifying={isSpecifying(currentLeg, canChange)}
/>
<span>{arrTime(currentLeg, canChange)}</span>
<TimeCell time={arrTime(currentLeg, canChange)} dayChange={arrDayChange(currentLeg, canChange)} />
<Section
legNumber={index + 2}
duration={nextLeg.flyingTime}
specifying={isSpecifying(nextLeg, canChange)}
/>
<span>{depTime(nextLeg, canChange)}</span>
<span>{arrTime(nextLeg, canChange)}</span>
<TimeCell time={depTime(nextLeg, canChange)} dayChange={depDayChange(nextLeg, canChange)} />
<TimeCell time={arrTime(nextLeg, canChange)} dayChange={arrDayChange(nextLeg, canChange)} />
</div>
<div className="timeline__row timeline__row--stations">