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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user