360 lines
13 KiB
TypeScript
360 lines
13 KiB
TypeScript
import { test, expect } from "./fixtures/console-gate";
|
|
import { formatYmd } from "./helpers/dates";
|
|
import {
|
|
routeDictionaryFixtures,
|
|
routeOnlineboardRouteFixtures,
|
|
} from "./helpers/api-fixtures";
|
|
|
|
test.describe("Online Board", () => {
|
|
test("/ru/onlineboard renders the start page with search form", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
// The OnlineBoardStartPage component should render
|
|
await expect(page.locator('[data-testid="online-board-start"]')).toBeVisible(
|
|
{ timeout: 10000 },
|
|
);
|
|
|
|
// The search form should be present (inside the default Flight Number accordion tab)
|
|
await expect(page.locator('[data-testid="search-form"]')).toBeVisible();
|
|
});
|
|
|
|
test("filter has accordion with Flight Number and Route tabs", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="filter-accordion"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Check both accordion tabs exist
|
|
await expect(page.locator('[data-testid="search-type-flight"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="search-type-route"]')).toBeVisible();
|
|
|
|
// Route tab is expanded by default — route inputs visible
|
|
await expect(
|
|
page.locator('[data-testid="route-departure-input"]'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("clicking Flight Number tab switches to flight form", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="filter-accordion"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Initially in "route" mode, route inputs should be visible
|
|
await expect(
|
|
page.locator('[data-testid="route-departure-input"]'),
|
|
).toBeVisible();
|
|
|
|
// Click "Flight Number" accordion header
|
|
await page.locator('[data-testid="search-type-flight"] a').click();
|
|
|
|
// Route inputs should disappear, flight number input should appear
|
|
await expect(
|
|
page.locator('[data-testid="route-departure-input"]'),
|
|
).not.toBeVisible();
|
|
await expect(
|
|
page.locator('[data-testid="flight-number-input"]'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("search form has route inputs, date picker, and submit button", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="search-form"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Route mode is default — departure and arrival inputs visible
|
|
await expect(
|
|
page.locator('[data-testid="route-departure-input"]'),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator('[data-testid="route-arrival-input"]'),
|
|
).toBeVisible();
|
|
|
|
// Date input
|
|
await expect(page.locator('[data-testid="date-input"]')).toBeVisible();
|
|
|
|
// Submit button
|
|
await expect(page.locator('[data-testid="search-submit"]')).toBeVisible();
|
|
});
|
|
|
|
test("flight number clear button clears the input", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="filter-accordion"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Switch to flight number tab first (route is default)
|
|
await page.locator('[data-testid="search-type-flight"] a').click();
|
|
await expect(page.locator('[data-testid="flight-number-input"]')).toBeVisible();
|
|
|
|
// Type a flight number
|
|
await page.locator('[data-testid="flight-number-input"]').fill("1234");
|
|
await expect(page.locator('[data-testid="flight-number-input"]')).toHaveValue("1234");
|
|
|
|
// Click clear button
|
|
await page.locator('[data-testid="flight-number-clear"]').click();
|
|
await expect(page.locator('[data-testid="flight-number-input"]')).toHaveValue("");
|
|
});
|
|
|
|
test("route tab has swap button and time selector", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="filter-accordion"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Switch to Route tab
|
|
await page.locator('[data-testid="search-type-route"] a').click();
|
|
|
|
// Swap button should be visible
|
|
await expect(page.locator('[data-testid="swap-cities-button"]')).toBeVisible();
|
|
|
|
// Time selector should be visible
|
|
await expect(page.locator('[data-testid="time-selector"]')).toBeVisible();
|
|
});
|
|
|
|
test("breadcrumbs are visible on start page", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="online-board-start"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
await expect(page.locator('[data-testid="breadcrumbs"]')).toBeVisible();
|
|
});
|
|
|
|
// FeedbackButton component exists but is not wired into OnlineBoardStartPage yet
|
|
test.fixme("feedback button is visible", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="online-board-start"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
await expect(page.locator('[data-testid="feedback-button"]')).toBeVisible();
|
|
});
|
|
|
|
test("/ru/onlineboard/flight/SU0100-{today} renders the flight search page", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
const today = formatYmd(new Date());
|
|
await page.goto(`/ru/onlineboard/flight/SU0100-${today}`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
// Should stay on the flight search URL
|
|
expect(page.url()).toContain(`/onlineboard/flight/SU0100-${today}`);
|
|
|
|
// Page should render something (either results, loading, or error state)
|
|
await expect(page.locator("body")).not.toBeEmpty();
|
|
});
|
|
|
|
test("/ru/onlineboard/departure/SVO-{today} renders the departure search page", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
const today = formatYmd(new Date());
|
|
await page.goto(`/ru/onlineboard/departure/SVO-${today}`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
expect(page.url()).toContain(`/onlineboard/departure/SVO-${today}`);
|
|
await expect(page.locator("body")).not.toBeEmpty();
|
|
});
|
|
|
|
test("/ru/onlineboard/route/SVO-LED-{today} renders the route search page", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
const today = formatYmd(new Date());
|
|
await page.goto(`/ru/onlineboard/route/SVO-LED-${today}`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
expect(page.url()).toContain(`/onlineboard/route/SVO-LED-${today}`);
|
|
await expect(page.locator("body")).not.toBeEmpty();
|
|
});
|
|
|
|
test("flight details page at /ru/onlineboard/SU0100-{today} renders", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
const today = formatYmd(new Date());
|
|
await page.goto(`/ru/onlineboard/SU0100-${today}`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
expect(page.url()).toContain(`/onlineboard/SU0100-${today}`);
|
|
await expect(page.locator("body")).not.toBeEmpty();
|
|
});
|
|
|
|
// Requires live API (city autocomplete + calendar days).
|
|
// Skipped when WAF blocks flights.test.aeroflot.ru.
|
|
test.skip("route search via form navigates to correct URL", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page.locator('[data-testid="filter-accordion"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Switch to Route tab
|
|
await page.locator('[data-testid="search-type-route"] a').click();
|
|
await expect(
|
|
page.locator('[data-testid="route-departure-input"]'),
|
|
).toBeVisible();
|
|
|
|
// Type departure city and wait for autocomplete dropdown
|
|
const depInput = page.locator('[data-testid="route-departure-input"]').getByRole("combobox");
|
|
await depInput.pressSequentially("Москва", { delay: 80 });
|
|
await expect(page.getByRole("option", { name: "Москва" })).toBeVisible({ timeout: 10000 });
|
|
await page.getByRole("option", { name: "Москва" }).click();
|
|
|
|
// Type arrival city and wait for autocomplete dropdown
|
|
const arrInput = page.locator('[data-testid="route-arrival-input"]').getByRole("combobox");
|
|
await arrInput.pressSequentially("Самара", { delay: 80 });
|
|
await expect(page.getByRole("option", { name: "Самара" })).toBeVisible({ timeout: 10000 });
|
|
await page.getByRole("option", { name: "Самара" }).click();
|
|
|
|
// Submit
|
|
await page.locator('[data-testid="search-submit"]').click();
|
|
|
|
// Should navigate to route URL
|
|
await page.waitForURL(/\/ru\/onlineboard\/route\/MOW-KUF-\d{8}/, { timeout: 15000 });
|
|
expect(page.url()).toMatch(/\/ru\/onlineboard\/route\/MOW-KUF-\d{8}/);
|
|
});
|
|
|
|
test("route search results page hydrates filter from URL params", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await routeDictionaryFixtures(page);
|
|
await routeOnlineboardRouteFixtures(page);
|
|
await page.goto(`/ru/onlineboard/route/MOW-KUF-${formatYmd(new Date())}`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="filter-accordion"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Route tab should be active and fields populated with user-facing city labels.
|
|
const depInput = page.locator('[data-testid="route-departure-input"]').getByRole("combobox");
|
|
await expect(depInput).toHaveValue("Москва");
|
|
|
|
const arrInput = page.locator('[data-testid="route-arrival-input"]').getByRole("combobox");
|
|
await expect(arrInput).toHaveValue("Самара");
|
|
});
|
|
|
|
test("route results show stale-data overlay after inactivity timeout", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
const target = window as unknown as { __ENV__?: Record<string, string> };
|
|
target.__ENV__ = {
|
|
...(target.__ENV__ ?? {}),
|
|
REFRESH_PAUSE_MIN: "0.01",
|
|
REFRESH_STOP_MIN: "1",
|
|
};
|
|
});
|
|
await routeDictionaryFixtures(page);
|
|
await routeOnlineboardRouteFixtures(page);
|
|
|
|
await page.goto(`/ru/onlineboard/route/MOW-KUF-${formatYmd(new Date())}`);
|
|
await expect(page.locator('[data-testid="online-board-search"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
const overlay = page.locator('[data-testid="stale-data-overlay"]');
|
|
await expect(overlay).toHaveText(
|
|
"Данные устарели, обновите страницу!",
|
|
{ timeout: 2000 },
|
|
);
|
|
await expect(overlay).toHaveCSS("position", "fixed");
|
|
await expect(overlay).toHaveCSS("top", "0px");
|
|
await expect(overlay).toHaveCSS("left", "0px");
|
|
await expect(overlay).toHaveCSS("text-align", "center");
|
|
await expect(overlay).toHaveCSS("background-color", "rgba(255, 255, 255, 0.7)");
|
|
await expect(overlay).toHaveCSS("z-index", "9999");
|
|
});
|
|
|
|
test("route results redirect to main page after stale-data stop timeout", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
const target = window as unknown as { __ENV__?: Record<string, string> };
|
|
target.__ENV__ = {
|
|
...(target.__ENV__ ?? {}),
|
|
REFRESH_PAUSE_MIN: "0.001",
|
|
REFRESH_STOP_MIN: "0.02",
|
|
};
|
|
});
|
|
await routeDictionaryFixtures(page);
|
|
await routeOnlineboardRouteFixtures(page);
|
|
|
|
await page.goto(`/ru/onlineboard/route/MOW-KUF-${formatYmd(new Date())}`);
|
|
await expect(page.locator('[data-testid="online-board-search"]')).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
|
|
await page.waitForURL(/\/(ru\/onlineboard)?$/, { timeout: 4000 });
|
|
});
|
|
|
|
// Requires live API (calendar days endpoint).
|
|
// Skipped when WAF blocks flights.test.aeroflot.ru.
|
|
test.skip("route search results page shows calendar strip with day numbers", async ({
|
|
page,
|
|
consoleMessages,
|
|
}) => {
|
|
await page.goto(`/ru/onlineboard/route/MOW-KUF-${formatYmd(new Date())}`);
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Calendar strip appears after client-side hydration fetches days API
|
|
const calendarStrip = page.locator('[data-testid="calendar-strip"]');
|
|
await expect(calendarStrip).toBeVisible({ timeout: 20000 });
|
|
|
|
// Should contain day number buttons (not raw bitmask "1111...")
|
|
const buttons = calendarStrip.locator("button");
|
|
const count = await buttons.count();
|
|
expect(count).toBeGreaterThan(0);
|
|
|
|
// Each button should show a short day number (1-31), not a long string
|
|
const firstButtonText = await buttons.first().textContent();
|
|
expect(firstButtonText!.trim().length).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
// TODO: SeoHead does not currently populate <title> on this route.
|
|
// Re-enable once the SeoHead component writes to document.title or uses <Helmet>.
|
|
test.fixme("page title is set on /ru/onlineboard", async ({ page, consoleMessages }) => {
|
|
await page.goto("/ru/onlineboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
await expect(page.locator('[data-testid="online-board-start"]')).toBeVisible(
|
|
{ timeout: 10000 },
|
|
);
|
|
|
|
const title = await page.title();
|
|
expect(title.length).toBeGreaterThan(0);
|
|
});
|
|
});
|