Schedule details: summary header, fix mini-list duplicates, fix timeline times
The schedule details page now renders Angular's <schedule-details-header> summary block (badges per flight + share/last-update + full-route timeline) between the day-tabs strip and the per-leg cards, so a connecting itinerary like SU 6188 + SU 6341 surfaces both flight numbers and the combined Moscow→Murmansk timeline up top instead of jumping straight from the date tabs to the first-leg detail card. Mini-list duplicate fix: when the sibling search returned 0 matches the fallback path used to leak the URL-parsed per-leg breakdown into the rail, producing a first-leg-only row stacked next to the synthesized combined row. Now the fallback is empty — the mini-list just shows the (synthesized) current flight on its own. FullRouteTimeline now uses the API's pre-formatted .localTime instead of the full ISO .local, so 00:30 / 02:00 shows up instead of 2026-04-26T00:30:00+03:00. useAppSettings.buyTicketMaxHours: parse <n>d as well as <n>h (Angular ships 330d for buyPeriod.max). Without this the Buy button hides for any flight more than ~3 days out. Plumbed sortMode/onSortChange/hideColumnHeaders through DayGroupedFlightList so the sticky ScheduleColumnHeaders and the inner list stay in sync (removes 2 TS errors in ScheduleSearchPage).
This commit is contained in:
@@ -1,50 +1,52 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// On the schedule details page, the left mini-list must show only the
|
||||
// CURRENTLY-OPEN flight's instance on each day in the [-1, +1] window —
|
||||
// matching Angular's `CurrentScheduleService.getScheduleType` /
|
||||
// `compareFlightsByPId` filtering. The old behaviour dumped the entire
|
||||
// MOW→MMK route search into the rail (every flight number, every
|
||||
// option), making the rail useless when the user came from the
|
||||
// search-results list.
|
||||
// On the schedule details page, the left mini-list must:
|
||||
// 1. Show only the CURRENTLY-OPEN flight's instance on each day in the
|
||||
// [-1, +1] window — matching Angular's
|
||||
// `CurrentScheduleService.getScheduleType` / `compareFlightsByPId`
|
||||
// filtering. The old behaviour dumped the entire MOW→MMK route
|
||||
// search into the rail.
|
||||
// 2. Render the open day's row with the SAME combined origin/
|
||||
// destination as its day-±1 siblings — for connecting itineraries
|
||||
// that means Moscow→Murmansk on every row, not first-leg-only
|
||||
// Moscow→St Petersburg on the highlighted row. Earlier we passed
|
||||
// `flights[0]` (the first leg) as `currentFlight`, which produced
|
||||
// a stub row that visually disagreed with the rest of the rail.
|
||||
//
|
||||
// Reference URL: connecting itinerary SU 6188 + SU 6341 (Moscow → St
|
||||
// Petersburg → Murmansk) on 2026-04-26. Each visible day in the
|
||||
// mini-list must list ≤ 1 entry — the same SU 6188 itinerary.
|
||||
// Petersburg → Murmansk) on 2026-04-26.
|
||||
|
||||
const URL =
|
||||
"/ru-ru/schedule/VKO/SU6188-20260426/LED/SU6341-20260427/MMK?request=schedule-route-MOW-MMK-20260427-20260503";
|
||||
|
||||
test("mini-list shows only the open flight per day, not the full route search", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("mini-list — flat list scoped to the open SU 6188 itinerary", async ({ page }) => {
|
||||
await page.goto(URL);
|
||||
|
||||
// Wait for the mini-list to render.
|
||||
const miniList = page.locator(".schedule-mini-list");
|
||||
await expect(miniList).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Wait until the day-headers appear (they exist for both the today
|
||||
// and ±1 days). Three day headers are expected total.
|
||||
const dayHeaders = miniList.locator("[data-testid^='mini-list-day-header-']");
|
||||
await expect(dayHeaders).toHaveCount(3);
|
||||
// Day-grouping accordions were removed — rows are flat.
|
||||
await expect(miniList.locator("[data-testid^='mini-list-day-header-']")).toHaveCount(0);
|
||||
|
||||
// The expanded body must contain at most one flight entry — the
|
||||
// open SU 6188 itinerary. The old behaviour rendered 4+ entries
|
||||
// (every MOW-MMK option for that day).
|
||||
const openBody = miniList
|
||||
.locator("[data-testid^='mini-list-day-']:not([data-testid*='header'])")
|
||||
.first();
|
||||
await expect(openBody).toBeVisible({ timeout: 10000 });
|
||||
// Mini-list items use SU 6188 in their visible label.
|
||||
const items = openBody.locator(".flights-mini-list-item, [class*='mini-list'] [class*='flight']");
|
||||
// Loose assertion — there should be at MOST one entry per day, and
|
||||
// the visible text must include 'SU 6188' (NOT a different
|
||||
// route-mate flight number).
|
||||
const text = (await openBody.innerText()).replace(/\s+/g, " ");
|
||||
expect(text).toContain("SU 6188");
|
||||
// Sanity: should NOT contain other Sunday MOW-MMK flight numbers
|
||||
// that the old listing pulled in (SU 6190 / SU 6699 are typical).
|
||||
expect(text).not.toMatch(/SU\s*6190/);
|
||||
expect(text).not.toMatch(/SU\s*6699/);
|
||||
const items = miniList.locator("[data-testid^='mini-list-item-']");
|
||||
await expect(items.first()).toBeVisible({ timeout: 10000 });
|
||||
// [-1, 0, +1] window for a daily itinerary: 3 rows, all SU 6188.
|
||||
await expect(items).toHaveCount(3);
|
||||
|
||||
// Every row must reference SU 6188 and must NOT contain unrelated
|
||||
// MOW-MMK route-mates (SU 6190 / SU 6699 used to leak in when the
|
||||
// rail was unfiltered).
|
||||
const railText = (await miniList.innerText()).replace(/\s+/g, " ");
|
||||
expect(railText).toContain("SU 6188");
|
||||
expect(railText).not.toMatch(/SU\s*6190/);
|
||||
expect(railText).not.toMatch(/SU\s*6699/);
|
||||
|
||||
// The current row (highlighted) must show the same combined
|
||||
// destination as its siblings — Murmansk, not St Petersburg
|
||||
// (otherwise the highlighted row collapses to the first leg only).
|
||||
const cityCount = (railText.match(/Мурманск/g) ?? []).length;
|
||||
expect(cityCount).toBeGreaterThanOrEqual(3);
|
||||
// No row should show St Petersburg as an arrival (that would mean
|
||||
// the current row regressed to first-leg-only).
|
||||
expect(railText).not.toContain("Санкт-Петербург");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// Schedule details page must render Angular's `<schedule-details-header>`
|
||||
// summary block between the day-tabs strip and the per-leg cards:
|
||||
//
|
||||
// • One `details-header-badge` per flight in the URL (so a connecting
|
||||
// itinerary like SU 6188 + SU 6341 shows BOTH badges, not just one).
|
||||
// • A right-side cluster with the share/buy/last-update controls.
|
||||
// • For multi-leg trips, a `flight-details-full-route` timeline that
|
||||
// spans every leg, with formatted times (`00:30`, not raw ISO).
|
||||
//
|
||||
// This test pins those guarantees so the page can't regress to "no
|
||||
// summary header" or "first-leg-only badge / raw ISO timeline" again.
|
||||
|
||||
const URL =
|
||||
"/ru-ru/schedule/VKO/SU6188-20260426/LED/SU6341-20260427/MMK?request=schedule-route-MOW-MMK-20260427-20260503";
|
||||
|
||||
test("summary header — both badges + last-update + formatted full-route timeline", async ({ page }) => {
|
||||
await page.goto(URL);
|
||||
|
||||
const summary = page.locator(".schedule-details__summary");
|
||||
await expect(summary).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Both flight-number badges present (SU 6188 + SU 6341).
|
||||
const badges = summary.locator(".details-header-badge");
|
||||
await expect(badges).toHaveCount(2);
|
||||
await expect(summary).toContainText("SU 6188");
|
||||
await expect(summary).toContainText("SU 6341");
|
||||
|
||||
// Last-update line is rendered (right-side cluster).
|
||||
await expect(summary).toContainText(/Последнее обновление:\s*\d{2}:\d{2}/);
|
||||
|
||||
// Full-route timeline is present and shows formatted clock times
|
||||
// (00:30 / 02:00 / 06:30 / 08:20) — NOT raw ISO timestamps with
|
||||
// T-separators or '+03:00' offsets, which was a regression from
|
||||
// using `.local` instead of `.localTime`.
|
||||
const timeline = summary.locator(".full-route-timeline");
|
||||
await expect(timeline).toBeVisible();
|
||||
const timelineText = (await timeline.innerText()).replace(/\s+/g, " ");
|
||||
expect(timelineText).toMatch(/\b00:30\b/);
|
||||
expect(timelineText).toMatch(/\b02:00\b/);
|
||||
expect(timelineText).toMatch(/\b08:20\b/);
|
||||
expect(timelineText).not.toMatch(/2026-04-\d{2}T/);
|
||||
expect(timelineText).not.toContain("+03:00");
|
||||
});
|
||||
Reference in New Issue
Block a user