Fix Дни выполнения рейса parsing: digit-list, not bitmask

API returns daysOfWeek.flight as a string of ISO weekday digits
("1"=Mon.."7"=Sun) where each character is the number of one
operating day, NOT a 7-char position bitmask. E.g. SU 6188's
flight value is "156" → Mon + Fri + Sat operating; SU 6341's is
"1234567" → daily. The old reader treated it as bitmask and only
checked position[i]==='1', so SU 6188 highlighted only Mon and SU
6341 highlighted only Mon — the highlighting looked random
relative to Angular which uses the digit-list semantics.

Walk the input character-by-character, build a Set<weekdayNumber>,
mark badge active when its day-number is in the set. Defends
against non-digit characters and out-of-range digits. Rewrite the
unit tests to match the real wire format and add a regression
case for the SU 6188 "156" pattern.
This commit is contained in:
2026-04-23 15:25:53 +03:00
parent 4c79914313
commit 013ca3ed91
3 changed files with 49 additions and 34 deletions
@@ -9,13 +9,17 @@ vi.mock("@/i18n/provider.js", () => ({
describe("DaysOfWeekStrip", () => {
it("renders 7 day boxes", () => {
render(<DaysOfWeekStrip flightBitString="1111111" />);
render(<DaysOfWeekStrip flightBitString="1234567" />);
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" />);
// Real API shape: `daysOfWeek.flight` is a STRING OF DIGITS where each
// character is the ISO weekday number of an active day (1=Mon..7=Sun).
// E.g. "156" means Mon + Fri + Sat are operating; "1234567" = every day.
it("marks Mon/Tue/Wed active, Thu-Sun inactive for '123'", () => {
render(<DaysOfWeekStrip flightBitString="123" />);
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/);
@@ -23,57 +27,52 @@ describe("DaysOfWeekStrip", () => {
expect(screen.getByTestId("day-of-week-6").className).toMatch(/--inactive/);
});
it("marks all active for '1111111'", () => {
render(<DaysOfWeekStrip flightBitString="1111111" />);
it("marks all active for '1234567'", () => {
render(<DaysOfWeekStrip flightBitString="1234567" />);
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" />);
it("marks all inactive for ''", () => {
render(<DaysOfWeekStrip flightBitString="" />);
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" />);
render(<DaysOfWeekStrip flightBitString="1234567" />);
expect(screen.getByTestId("day-of-week-0").textContent).toContain("DAYS.1");
expect(screen.getByTestId("day-of-week-6").textContent).toContain("DAYS.7");
});
// TZ §4.1.16.8: position 0 = Monday (first ISO weekday), position 6 = Sunday
// Verify the Mon/Wed/Fri pattern used by typical schedule flights ("Пн Ср Пт" = "1010100")
it("Mon/Wed/Fri pattern: positions 0,2,4 active; 1,3,5,6 inactive", () => {
render(<DaysOfWeekStrip flightBitString="1010100" />);
// Active: Mon(0), Wed(2), Fri(4)
// Real-world: SU 6188 returns "156" → Mon, Fri, Sat operating.
it("Mon/Fri/Sat pattern '156': positions 0,4,5 active; 1,2,3,6 inactive", () => {
render(<DaysOfWeekStrip flightBitString="156" />);
expect(screen.getByTestId("day-of-week-0").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-2").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-4").className).not.toMatch(/--inactive/);
// Inactive: Tue(1), Thu(3), Sat(5), Sun(6)
expect(screen.getByTestId("day-of-week-5").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-1").className).toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-2").className).toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-3").className).toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-5").className).toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-6").className).toMatch(/--inactive/);
});
// TZ §4.1.16.8: Sat/Sun only ("0000011")
it("weekend-only pattern: positions 5,6 active; 0-4 inactive", () => {
render(<DaysOfWeekStrip flightBitString="0000011" />);
for (let i = 0; i < 5; i++) {
expect(screen.getByTestId(`day-of-week-${i}`).className).toMatch(/--inactive/);
}
expect(screen.getByTestId("day-of-week-5").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-6").className).not.toMatch(/--inactive/);
});
// TZ §4.1.16.8: single-day operation (Tuesday only = "0100000")
it("Tuesday-only pattern: position 1 active; all others inactive", () => {
render(<DaysOfWeekStrip flightBitString="0100000" />);
it("Tue-only '2': position 1 active; all others inactive", () => {
render(<DaysOfWeekStrip flightBitString="2" />);
expect(screen.getByTestId("day-of-week-1").className).not.toMatch(/--inactive/);
for (const i of [0, 2, 3, 4, 5, 6]) {
expect(screen.getByTestId(`day-of-week-${i}`).className).toMatch(/--inactive/);
}
});
it("ignores non-digit characters and out-of-range digits", () => {
render(<DaysOfWeekStrip flightBitString="1x9 7" />);
expect(screen.getByTestId("day-of-week-0").className).not.toMatch(/--inactive/);
expect(screen.getByTestId("day-of-week-6").className).not.toMatch(/--inactive/);
for (const i of [1, 2, 3, 4, 5]) {
expect(screen.getByTestId(`day-of-week-${i}`).className).toMatch(/--inactive/);
}
});
});
@@ -2,6 +2,14 @@ import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
export interface DaysOfWeekStripProps {
/**
* Operating-days indicator from the API. The `daysOfWeek.flight` field
* is a STRING OF DIGITS (1=Mon..7=Sun), one digit per active day —
* NOT a 7-char bitmask. Examples: `"156"` = Mon/Fri/Sat;
* `"1234567"` = every day; `""` = none. Anything else (alternative
* shape, missing field) falls through to "all inactive" so the badge
* row still renders without crashing.
*/
flightBitString: string;
}
@@ -9,11 +17,19 @@ const DAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const;
export const DaysOfWeekStrip: FC<DaysOfWeekStripProps> = ({ flightBitString }) => {
const { t } = useTranslation();
// Build a Set<dayNumber> from the API string. Each character is a
// 1-based ISO weekday digit ("1" → Mon, …, "7" → Sun).
const activeDays = new Set<number>();
for (const ch of flightBitString) {
const n = Number(ch);
if (Number.isInteger(n) && n >= 1 && n <= 7) activeDays.add(n);
}
return (
<div className="days-of-week-strip">
{DAY_INDEXES.map((i) => {
const isActive = flightBitString[i] === "1";
const dayNumber = i + 1;
const isActive = activeDays.has(dayNumber);
const className = isActive ? "day" : "day day--inactive";
return (
<span
@@ -21,7 +37,7 @@ export const DaysOfWeekStrip: FC<DaysOfWeekStripProps> = ({ flightBitString }) =
className={className}
data-testid={`day-of-week-${i}`}
>
{t(`DAYS.${i + 1}`)}
{t(`DAYS.${dayNumber}`)}
</span>
);
})}
@@ -159,10 +159,10 @@ describe("FlightSchedule", () => {
});
// TZ §4.1.16.8: uses daysOfWeek.flight (schedule days), not .current (today's active day)
it("uses daysOfWeek.flight bitstring for active days, not .current", () => {
// current = daily, flight = Mon/Wed/Fri only ("1010100")
it("uses daysOfWeek.flight digit-list for active days, not .current", () => {
// current = daily ("1234567"), flight = Mon/Wed/Fri only ("135")
const flight = makeFlight({
daysOfWeek: { current: "1111111", flight: "1010100" },
daysOfWeek: { current: "1234567", flight: "135" },
});
render(<FlightSchedule flight={flight} />);
// Mon(0), Wed(2), Fri(4) should be active