This commit is contained in:
@@ -35,7 +35,6 @@ export const BoardDetailsHeader: FC<BoardDetailsHeaderProps> = ({ flight, locale
|
|||||||
<FlightActions
|
<FlightActions
|
||||||
flight={flight}
|
flight={flight}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
showShare
|
|
||||||
showPrint={false}
|
showPrint={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { FlightActions } from "./FlightActions.js";
|
import { FlightActions } from "./FlightActions.js";
|
||||||
import type { ISimpleFlight } from "../../types.js";
|
import type { ISimpleFlight } from "../../types.js";
|
||||||
|
|
||||||
@@ -78,9 +78,9 @@ function makeFlight(): ISimpleFlight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("FlightActions", () => {
|
describe("FlightActions", () => {
|
||||||
it("renders share, buy, register by default (status hidden)", () => {
|
it("renders buy and register by default, without share or status", () => {
|
||||||
render(<FlightActions flight={makeFlight()} locale="ru" />);
|
render(<FlightActions flight={makeFlight()} locale="ru" />);
|
||||||
expect(screen.getByTestId("share-button")).toBeTruthy();
|
expect(screen.queryByTestId("share-button")).toBeNull();
|
||||||
expect(screen.getByTestId("buy-ticket-button")).toBeTruthy();
|
expect(screen.getByTestId("buy-ticket-button")).toBeTruthy();
|
||||||
expect(screen.getByTestId("registration-button")).toBeTruthy();
|
expect(screen.getByTestId("registration-button")).toBeTruthy();
|
||||||
expect(screen.queryByTestId("flight-status-button")).toBeNull();
|
expect(screen.queryByTestId("flight-status-button")).toBeNull();
|
||||||
@@ -91,11 +91,6 @@ describe("FlightActions", () => {
|
|||||||
expect(screen.getByTestId("flight-status-button")).toBeTruthy();
|
expect(screen.getByTestId("flight-status-button")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides share button when showShare=false", () => {
|
|
||||||
render(<FlightActions flight={makeFlight()} locale="ru" showShare={false} />);
|
|
||||||
expect(screen.queryByTestId("share-button")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides buy ticket when canBuyTicket returns false", async () => {
|
it("hides buy ticket when canBuyTicket returns false", async () => {
|
||||||
const mod = await import("./visibility/buyTicketVisibility.js");
|
const mod = await import("./visibility/buyTicketVisibility.js");
|
||||||
vi.mocked(mod.canBuyTicket).mockReturnValue(false);
|
vi.mocked(mod.canBuyTicket).mockReturnValue(false);
|
||||||
@@ -108,15 +103,4 @@ describe("FlightActions", () => {
|
|||||||
render(<FlightActions flight={makeFlight()} locale="ru" />);
|
render(<FlightActions flight={makeFlight()} locale="ru" />);
|
||||||
expect(screen.getByTestId("flight-actions")).toBeTruthy();
|
expect(screen.getByTestId("flight-actions")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shares the flight details URL instead of the current page URL", () => {
|
|
||||||
window.history.pushState({}, "", "/ru-ru/schedule/SVO/SU0022-20260417/LED?request=schedule-route-MOW-LED-20260413-20260419");
|
|
||||||
render(<FlightActions flight={makeFlight()} locale="ru-ru" viewType="Schedule" />);
|
|
||||||
fireEvent.click(screen.getByTestId("share-button"));
|
|
||||||
const link = screen.getByTestId("share-vk") as HTMLAnchorElement;
|
|
||||||
expect(decodeURIComponent(link.href)).toContain(
|
|
||||||
`${window.location.origin}/ru-ru/schedule/SVO/SU0022-20260417/LED`,
|
|
||||||
);
|
|
||||||
expect(decodeURIComponent(link.href)).not.toContain("request=schedule-route");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,16 +8,13 @@ import { canViewFlightStatus } from "./visibility/flightStatusVisibility.js";
|
|||||||
import { BuyTicketButton } from "./BuyTicketButton.js";
|
import { BuyTicketButton } from "./BuyTicketButton.js";
|
||||||
import { RegistrationButton } from "./RegistrationButton.js";
|
import { RegistrationButton } from "./RegistrationButton.js";
|
||||||
import { FlightStatusButton } from "./FlightStatusButton.js";
|
import { FlightStatusButton } from "./FlightStatusButton.js";
|
||||||
import { ShareButton } from "./ShareButton.js";
|
|
||||||
import { PrintButton } from "./PrintButton.js";
|
import { PrintButton } from "./PrintButton.js";
|
||||||
import { buildFlightShareUrl, type FlightShareViewType } from "./shareUrl.js";
|
|
||||||
|
|
||||||
export interface FlightActionsProps {
|
export interface FlightActionsProps {
|
||||||
flight: ISimpleFlight;
|
flight: ISimpleFlight;
|
||||||
locale: string;
|
locale: string;
|
||||||
showStatus?: boolean;
|
showStatus?: boolean;
|
||||||
showPrint?: boolean;
|
showPrint?: boolean;
|
||||||
showShare?: boolean;
|
|
||||||
showRegister?: boolean;
|
showRegister?: boolean;
|
||||||
showBuy?: boolean;
|
showBuy?: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +25,6 @@ export interface FlightActionsProps {
|
|||||||
* particular day's "Cancelled" status would hide a useful action.
|
* particular day's "Cancelled" status would hide a useful action.
|
||||||
*/
|
*/
|
||||||
forceBuy?: boolean;
|
forceBuy?: boolean;
|
||||||
viewType?: FlightShareViewType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlightActions: FC<FlightActionsProps> = ({
|
export const FlightActions: FC<FlightActionsProps> = ({
|
||||||
@@ -36,11 +32,9 @@ export const FlightActions: FC<FlightActionsProps> = ({
|
|||||||
locale,
|
locale,
|
||||||
showStatus = false,
|
showStatus = false,
|
||||||
showPrint = false,
|
showPrint = false,
|
||||||
showShare = true,
|
|
||||||
showRegister = true,
|
showRegister = true,
|
||||||
showBuy = true,
|
showBuy = true,
|
||||||
forceBuy = false,
|
forceBuy = false,
|
||||||
viewType = "Onlineboard",
|
|
||||||
}) => {
|
}) => {
|
||||||
const { flightStatusAvailableFromHours, buyTicketMinHours, buyTicketMaxHours } = useAppSettings();
|
const { flightStatusAvailableFromHours, buyTicketMinHours, buyTicketMaxHours } = useAppSettings();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -52,12 +46,9 @@ export const FlightActions: FC<FlightActionsProps> = ({
|
|||||||
showStatus &&
|
showStatus &&
|
||||||
canViewFlightStatus(flight, now, flightStatusAvailableFromHours, AIRLINES_WITH_STATUS);
|
canViewFlightStatus(flight, now, flightStatusAvailableFromHours, AIRLINES_WITH_STATUS);
|
||||||
|
|
||||||
const shareUrl = buildFlightShareUrl(flight, locale, viewType);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flight-actions" data-testid="flight-actions">
|
<div className="flight-actions" data-testid="flight-actions">
|
||||||
{showPrint && <PrintButton flight={flight} />}
|
{showPrint && <PrintButton flight={flight} />}
|
||||||
{showShare && <ShareButton url={shareUrl} locale={locale} />}
|
|
||||||
{canBuy && <BuyTicketButton flight={flight} locale={locale} />}
|
{canBuy && <BuyTicketButton flight={flight} locale={locale} />}
|
||||||
{canReg && <RegistrationButton flight={flight} />}
|
{canReg && <RegistrationButton flight={flight} />}
|
||||||
{canStatus && <FlightStatusButton flight={flight} locale={locale} />}
|
{canStatus && <FlightStatusButton flight={flight} locale={locale} />}
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ describe("LastUpdate", () => {
|
|||||||
expect(ts.textContent).toMatch(/\d{2}:\d{2}\s+\d{2}\.\d{2}\.\d{4}/);
|
expect(ts.textContent).toMatch(/\d{2}:\d{2}\s+\d{2}\.\d{2}\.\d{4}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders share button", () => {
|
it("does not render a share button", () => {
|
||||||
render(<LastUpdate flight={makeFlight("2026-04-20T12:34:00Z")} locale="ru" />);
|
render(<LastUpdate flight={makeFlight("2026-04-20T12:34:00Z")} locale="ru" />);
|
||||||
expect(screen.getByTestId("share-button")).toBeTruthy();
|
expect(screen.queryByTestId("share-button")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import { type FC, useEffect, useRef, useState } from "react";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useTranslation } from "@/i18n/provider.js";
|
import { useTranslation } from "@/i18n/provider.js";
|
||||||
import type { ISimpleFlight } from "../../types.js";
|
import type { ISimpleFlight } from "../../types.js";
|
||||||
import { ShareButton } from "./ShareButton.js";
|
|
||||||
import { buildFlightShareUrl, type FlightShareViewType } from "./shareUrl.js";
|
|
||||||
|
|
||||||
export interface LastUpdateProps {
|
export interface LastUpdateProps {
|
||||||
flight: ISimpleFlight;
|
flight: ISimpleFlight;
|
||||||
locale: string;
|
locale: string;
|
||||||
viewType?: FlightShareViewType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatStamp(d: Date): string {
|
function formatStamp(d: Date): string {
|
||||||
@@ -23,7 +20,7 @@ function formatStamp(d: Date): string {
|
|||||||
* timestamp. We mirror that here: capture `Date.now()` the first time we
|
* timestamp. We mirror that here: capture `Date.now()` the first time we
|
||||||
* see a given flight.id, then re-capture whenever the id changes.
|
* see a given flight.id, then re-capture whenever the id changes.
|
||||||
*/
|
*/
|
||||||
export const LastUpdate: FC<LastUpdateProps> = ({ flight, locale, viewType = "Onlineboard" }) => {
|
export const LastUpdate: FC<LastUpdateProps> = ({ flight }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loadedAt, setLoadedAt] = useState<Date>(() => new Date());
|
const [loadedAt, setLoadedAt] = useState<Date>(() => new Date());
|
||||||
const seenFlightIdRef = useRef<string | null>(null);
|
const seenFlightIdRef = useRef<string | null>(null);
|
||||||
@@ -36,11 +33,9 @@ export const LastUpdate: FC<LastUpdateProps> = ({ flight, locale, viewType = "On
|
|||||||
}, [flight.id]);
|
}, [flight.id]);
|
||||||
|
|
||||||
const timestamp = formatStamp(loadedAt);
|
const timestamp = formatStamp(loadedAt);
|
||||||
const shareUrl = buildFlightShareUrl(flight, locale, viewType);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="last-update">
|
<div className="last-update">
|
||||||
<ShareButton url={shareUrl} locale={locale} />
|
|
||||||
<span className="last-update__description">
|
<span className="last-update__description">
|
||||||
<span>{t("SHARED.LAST-UPDATE")}:</span>
|
<span>{t("SHARED.LAST-UPDATE")}:</span>
|
||||||
<span className="last-update__time" data-testid="last-update-timestamp">
|
<span className="last-update__time" data-testid="last-update-timestamp">
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// @vitest-environment jsdom
|
|
||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import { ShareButton } from "./ShareButton.js";
|
|
||||||
|
|
||||||
vi.mock("@/i18n/provider.js", () => ({
|
|
||||||
useTranslation: () => ({ t: (k: string) => k }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("ShareButton", () => {
|
|
||||||
it("renders button with testid share-button", () => {
|
|
||||||
render(<ShareButton url="https://example.com" locale="en" />);
|
|
||||||
expect(screen.getByTestId("share-button")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("panel is closed by default", () => {
|
|
||||||
render(<ShareButton url="https://example.com" locale="en" />);
|
|
||||||
expect(screen.queryByTestId("share-panel")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("click opens the share panel", () => {
|
|
||||||
render(<ShareButton url="https://example.com" locale="en" />);
|
|
||||||
fireEvent.click(screen.getByTestId("share-button"));
|
|
||||||
expect(screen.getByTestId("share-panel")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("second click closes the share panel", () => {
|
|
||||||
render(<ShareButton url="https://example.com" locale="en" />);
|
|
||||||
const btn = screen.getByTestId("share-button");
|
|
||||||
fireEvent.click(btn);
|
|
||||||
expect(screen.getByTestId("share-panel")).toBeTruthy();
|
|
||||||
fireEvent.click(btn);
|
|
||||||
expect(screen.queryByTestId("share-panel")).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { type FC, useState } from "react";
|
|
||||||
import { useTranslation } from "@/i18n/provider.js";
|
|
||||||
import { SharePanel } from "./SharePanel.js";
|
|
||||||
import "./actions.scss";
|
|
||||||
|
|
||||||
export interface ShareButtonProps {
|
|
||||||
url: string;
|
|
||||||
locale: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShareButton: FC<ShareButtonProps> = ({ url, locale }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="share-button-wrap">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flight-action-btn flight-action-btn--transparent"
|
|
||||||
data-testid="share-button"
|
|
||||||
title={t("BOARD.SHARE")}
|
|
||||||
onClick={() => setOpen((v) => !v)}
|
|
||||||
aria-label={t("BOARD.SHARE")}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="18" cy="5" r="3" />
|
|
||||||
<circle cx="6" cy="12" r="3" />
|
|
||||||
<circle cx="18" cy="19" r="3" />
|
|
||||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
|
||||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{open && <SharePanel url={url} locale={locale} onClose={() => setOpen(false)} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// @vitest-environment jsdom
|
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
||||||
import { SharePanel } from "./SharePanel.js";
|
|
||||||
|
|
||||||
vi.mock("@/i18n/provider.js", () => ({
|
|
||||||
useTranslation: () => ({ t: (k: string) => k }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("SharePanel", () => {
|
|
||||||
let writeTextMock: ReturnType<typeof vi.fn>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
writeTextMock = vi.fn().mockResolvedValue(undefined);
|
|
||||||
Object.defineProperty(navigator, "clipboard", {
|
|
||||||
value: { writeText: writeTextMock },
|
|
||||||
writable: true,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders 3 social links for en locale (no Weibo)", () => {
|
|
||||||
render(<SharePanel url="https://example.com/flight" locale="en" onClose={() => {}} />);
|
|
||||||
expect(screen.getByTestId("share-facebook")).toBeTruthy();
|
|
||||||
expect(screen.getByTestId("share-vk")).toBeTruthy();
|
|
||||||
expect(screen.getByTestId("share-twitter")).toBeTruthy();
|
|
||||||
expect(screen.queryByTestId("share-weibo")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders only Weibo social link for zh locale", () => {
|
|
||||||
render(<SharePanel url="https://example.com/flight" locale="zh" onClose={() => {}} />);
|
|
||||||
expect(screen.queryByTestId("share-facebook")).toBeNull();
|
|
||||||
expect(screen.queryByTestId("share-vk")).toBeNull();
|
|
||||||
expect(screen.queryByTestId("share-twitter")).toBeNull();
|
|
||||||
expect(screen.getByTestId("share-weibo")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders only Weibo social link for cn country locale", () => {
|
|
||||||
render(<SharePanel url="https://example.com/flight" locale="cn-en" onClose={() => {}} />);
|
|
||||||
expect(screen.queryByTestId("share-facebook")).toBeNull();
|
|
||||||
expect(screen.queryByTestId("share-vk")).toBeNull();
|
|
||||||
expect(screen.queryByTestId("share-twitter")).toBeNull();
|
|
||||||
expect(screen.getByTestId("share-weibo")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Facebook href contains encoded URL", () => {
|
|
||||||
render(<SharePanel url="https://example.com/flight?a=1" locale="en" onClose={() => {}} />);
|
|
||||||
const link = screen.getByTestId("share-facebook") as HTMLAnchorElement;
|
|
||||||
expect(link.href).toContain(encodeURIComponent("https://example.com/flight?a=1"));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("copy button calls navigator.clipboard.writeText and onClose", async () => {
|
|
||||||
const onClose = vi.fn();
|
|
||||||
render(<SharePanel url="https://example.com/flight" locale="en" onClose={onClose} />);
|
|
||||||
fireEvent.click(screen.getByTestId("share-copy"));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(writeTextMock).toHaveBeenCalledWith("https://example.com/flight");
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(onClose).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { type FC, type MouseEvent, useEffect } from "react";
|
|
||||||
import { useTranslation } from "@/i18n/provider.js";
|
|
||||||
import "./actions.scss";
|
|
||||||
|
|
||||||
export interface SharePanelProps {
|
|
||||||
url: string;
|
|
||||||
locale: string;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isChinaLocale(locale: string): boolean {
|
|
||||||
const normalized = locale.toLowerCase();
|
|
||||||
const [country, language] = normalized.split("-");
|
|
||||||
return country === "cn" || language === "zh" || normalized === "zh";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SharePanel: FC<SharePanelProps> = ({ url, locale, onClose }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const encoded = encodeURIComponent(url);
|
|
||||||
const chinaLocale = isChinaLocale(locale);
|
|
||||||
|
|
||||||
// Close on Escape — matches Angular's PrimeNG p-overlayPanel dismissable behaviour.
|
|
||||||
useEffect(() => {
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") onClose();
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const handleCopy = async (e: MouseEvent<HTMLAnchorElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(url);
|
|
||||||
onClose();
|
|
||||||
} catch {
|
|
||||||
// silent
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="share-panel" data-testid="share-panel" role="dialog" aria-label={t("BOARD.SHARE")}>
|
|
||||||
<div className="share-elements">
|
|
||||||
{!chinaLocale && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className="share-element facebook"
|
|
||||||
data-testid="share-facebook"
|
|
||||||
href={`https://www.facebook.com/sharer/sharer.php?u=${encoded}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t("SHARE.FACEBOOK")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className="share-element vk"
|
|
||||||
data-testid="share-vk"
|
|
||||||
href={`https://vk.com/share.php?url=${encoded}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t("SHARE.VK")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className="share-element twitter"
|
|
||||||
data-testid="share-twitter"
|
|
||||||
href={`https://twitter.com/share?text=${encodeURIComponent("My Flight")}&url=${encoded}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t("SHARE.TWITTER")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{chinaLocale && (
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className="share-element weibo"
|
|
||||||
data-testid="share-weibo"
|
|
||||||
href={`https://service.weibo.com/share/share.php?url=${encoded}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t("SHARE.WEIBO")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className="share-element copy"
|
|
||||||
data-testid="share-copy"
|
|
||||||
href="#"
|
|
||||||
onClick={handleCopy}
|
|
||||||
>
|
|
||||||
{t("SHARE.COPY")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// @vitest-environment jsdom
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { IFlightLeg, ISimpleFlight } from "../../types.js";
|
|
||||||
import { buildFlightShareUrl } from "./shareUrl.js";
|
|
||||||
|
|
||||||
function makeLeg(
|
|
||||||
departure: string,
|
|
||||||
arrival: string,
|
|
||||||
local: string,
|
|
||||||
): IFlightLeg {
|
|
||||||
return {
|
|
||||||
arrival: {
|
|
||||||
scheduled: { airport: "", airportCode: arrival, city: "", cityCode: "", countryCode: "" },
|
|
||||||
times: {
|
|
||||||
scheduledArrival: {
|
|
||||||
dayChange: { value: 0, title: "" },
|
|
||||||
local: "",
|
|
||||||
localTime: "",
|
|
||||||
tzOffset: 0,
|
|
||||||
utc: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dayChange: 0,
|
|
||||||
departure: {
|
|
||||||
scheduled: { airport: "", airportCode: departure, city: "", cityCode: "", countryCode: "" },
|
|
||||||
checkingStatus: "Scheduled",
|
|
||||||
times: {
|
|
||||||
scheduledDeparture: {
|
|
||||||
dayChange: { value: 0, title: "" },
|
|
||||||
local,
|
|
||||||
localTime: "",
|
|
||||||
tzOffset: 0,
|
|
||||||
utc: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
equipment: {},
|
|
||||||
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
|
|
||||||
flyingTime: "1h",
|
|
||||||
index: 0,
|
|
||||||
operatingBy: {},
|
|
||||||
status: "Scheduled",
|
|
||||||
updated: "",
|
|
||||||
} as IFlightLeg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDirectFlight(): ISimpleFlight {
|
|
||||||
return {
|
|
||||||
id: "direct",
|
|
||||||
routeType: "Direct",
|
|
||||||
flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260416" },
|
|
||||||
flyingTime: "1h",
|
|
||||||
operatingBy: {},
|
|
||||||
status: "Scheduled",
|
|
||||||
leg: makeLeg("SVO", "LED", "2026-04-17T10:00:00"),
|
|
||||||
} as ISimpleFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("buildFlightShareUrl", () => {
|
|
||||||
it("builds an online-board details URL from the flight local departure date", () => {
|
|
||||||
expect(buildFlightShareUrl(makeDirectFlight(), "ru-ru")).toBe(
|
|
||||||
`${window.location.origin}/ru-ru/onlineboard/SU0022-20260417`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds a clean schedule details URL without request query", () => {
|
|
||||||
expect(buildFlightShareUrl(makeDirectFlight(), "ru-ru", "Schedule")).toBe(
|
|
||||||
`${window.location.origin}/ru-ru/schedule/SVO/SU0022-20260417/LED`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds an airport-interleaved schedule URL for connecting itineraries", () => {
|
|
||||||
const flight = {
|
|
||||||
...makeDirectFlight(),
|
|
||||||
id: "connecting",
|
|
||||||
routeType: "MultiLeg",
|
|
||||||
flightId: { carrier: "SU", flightNumber: "5752", suffix: "", date: "20260529" },
|
|
||||||
legs: [
|
|
||||||
makeLeg("VVO", "KJA", "2026-05-29T11:00:00"),
|
|
||||||
makeLeg("KJA", "MJZ", "2026-05-30T08:00:00"),
|
|
||||||
],
|
|
||||||
_childFlightIds: [
|
|
||||||
{ carrier: "SU", flightNumber: "5752", suffix: "", date: "20260529" },
|
|
||||||
{ carrier: "SU", flightNumber: "6837", suffix: "", date: "20260530" },
|
|
||||||
],
|
|
||||||
} as unknown as ISimpleFlight;
|
|
||||||
|
|
||||||
expect(buildFlightShareUrl(flight, "ru-ru", "Schedule")).toBe(
|
|
||||||
`${window.location.origin}/ru-ru/schedule/VVO/SU5752-20260529/KJA/SU6837-20260530/MJZ`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { buildFlightUrlParams } from "../../url.js";
|
|
||||||
import { getFlightSearchDate } from "../../flightSearchDate.js";
|
|
||||||
import type { IFlightLeg, ISimpleFlight } from "../../types.js";
|
|
||||||
|
|
||||||
export type FlightShareViewType = "Onlineboard" | "Schedule";
|
|
||||||
|
|
||||||
function getOrigin(): string {
|
|
||||||
if (typeof window === "undefined") return "";
|
|
||||||
return window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLegs(flight: ISimpleFlight): IFlightLeg[] {
|
|
||||||
return flight.routeType === "Direct" ? [flight.leg] : flight.legs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function compactLocalDate(leg: IFlightLeg | undefined, fallback: string): string {
|
|
||||||
const local = leg?.departure.times.scheduledDeparture.local;
|
|
||||||
if (local) {
|
|
||||||
const date = local.slice(0, 10).replace(/-/g, "");
|
|
||||||
if (/^\d{8}$/.test(date)) return date;
|
|
||||||
}
|
|
||||||
return fallback.replace(/-/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildScheduleSegment(flight: ISimpleFlight): string {
|
|
||||||
const legs = getLegs(flight);
|
|
||||||
const childIds = (flight as ISimpleFlight & {
|
|
||||||
_childFlightIds?: Array<{
|
|
||||||
carrier: string;
|
|
||||||
flightNumber: string;
|
|
||||||
suffix?: string;
|
|
||||||
date?: string;
|
|
||||||
}>;
|
|
||||||
})._childFlightIds;
|
|
||||||
const ids = childIds && childIds.length > 0 ? childIds : [flight.flightId];
|
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
ids.forEach((id, index) => {
|
|
||||||
const leg = legs[index] ?? legs[legs.length - 1];
|
|
||||||
if (index === 0 && leg) parts.push(leg.departure.scheduled.airportCode);
|
|
||||||
parts.push(
|
|
||||||
buildFlightUrlParams({
|
|
||||||
carrier: id.carrier,
|
|
||||||
flightNumber: id.flightNumber,
|
|
||||||
...(id.suffix ? { suffix: id.suffix } : {}),
|
|
||||||
date: compactLocalDate(leg, id.date ?? flight.flightId.date),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (leg) parts.push(leg.arrival.scheduled.airportCode);
|
|
||||||
});
|
|
||||||
|
|
||||||
return parts.join("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildFlightShareUrl(
|
|
||||||
flight: ISimpleFlight,
|
|
||||||
locale: string,
|
|
||||||
viewType: FlightShareViewType = "Onlineboard",
|
|
||||||
): string {
|
|
||||||
const origin = getOrigin();
|
|
||||||
|
|
||||||
if (viewType === "Schedule") {
|
|
||||||
return `${origin}/${locale}/schedule/${buildScheduleSegment(flight)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${origin}/${locale}/onlineboard/${buildFlightUrlParams({
|
|
||||||
carrier: flight.flightId.carrier,
|
|
||||||
flightNumber: flight.flightId.flightNumber,
|
|
||||||
...(flight.flightId.suffix ? { suffix: flight.flightId.suffix } : {}),
|
|
||||||
date: getFlightSearchDate(flight),
|
|
||||||
})}`;
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,6 @@ import { buildOnlineBoardUrl } from "../url.js";
|
|||||||
import { buildFlightListJsonLd } from "../json-ld.js";
|
import { buildFlightListJsonLd } from "../json-ld.js";
|
||||||
import { sortFlights } from "../sortFlights.js";
|
import { sortFlights } from "../sortFlights.js";
|
||||||
import { getFlightSearchDate } from "../flightSearchDate.js";
|
import { getFlightSearchDate } from "../flightSearchDate.js";
|
||||||
import { buildFlightShareUrl } from "./BoardDetailsHeader/shareUrl.js";
|
|
||||||
import {
|
import {
|
||||||
PobedaAuroraBanner,
|
PobedaAuroraBanner,
|
||||||
shouldShowPobedaAuroraBanner,
|
shouldShowPobedaAuroraBanner,
|
||||||
@@ -620,7 +619,6 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
|||||||
onFlightClick={handleFlightClick}
|
onFlightClick={handleFlightClick}
|
||||||
initialCurrentFlightId={initialCurrentFlightId}
|
initialCurrentFlightId={initialCurrentFlightId}
|
||||||
direction={params.type}
|
direction={params.type}
|
||||||
shareUrlFor={(flight) => buildFlightShareUrl(flight, locale)}
|
|
||||||
renderActions={(flight) => (
|
renderActions={(flight) => (
|
||||||
// Mirrors Angular's board search expansion: each row
|
// Mirrors Angular's board search expansion: each row
|
||||||
// shows [Купить] [Онлайн регистрация] alongside the
|
// shows [Купить] [Онлайн регистрация] alongside the
|
||||||
@@ -630,7 +628,6 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
|||||||
<FlightActions
|
<FlightActions
|
||||||
flight={flight}
|
flight={flight}
|
||||||
locale={language}
|
locale={language}
|
||||||
showShare={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { FlightList } from "@/ui/flights/FlightList.js";
|
|||||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||||
import { useLocale } from "@/i18n/useLocale.js";
|
import { useLocale } from "@/i18n/useLocale.js";
|
||||||
import type { ISimpleFlight, IFlightLeg } from "@/features/online-board/types.js";
|
import type { ISimpleFlight, IFlightLeg } from "@/features/online-board/types.js";
|
||||||
import { buildFlightShareUrl } from "@/features/online-board/components/BoardDetailsHeader/shareUrl.js";
|
|
||||||
import { ScheduleFlightBody } from "./ScheduleFlightBody.js";
|
import { ScheduleFlightBody } from "./ScheduleFlightBody.js";
|
||||||
import type { ScheduleSortMode } from "./ScheduleColumnHeaders.js";
|
import type { ScheduleSortMode } from "./ScheduleColumnHeaders.js";
|
||||||
import "./DayGroupedFlightList.scss";
|
import "./DayGroupedFlightList.scss";
|
||||||
@@ -287,7 +286,6 @@ export const DayGroupedFlightList: FC<DayGroupedFlightListProps> = ({
|
|||||||
loading={false}
|
loading={false}
|
||||||
direction="schedule"
|
direction="schedule"
|
||||||
renderExpandedBody={renderScheduleBody}
|
renderExpandedBody={renderScheduleBody}
|
||||||
shareUrlFor={(flight) => buildFlightShareUrl(flight, locale, "Schedule")}
|
|
||||||
{...(onFlightClick ? { onFlightClick } : {})}
|
{...(onFlightClick ? { onFlightClick } : {})}
|
||||||
{...(buyUrlFor ? { buyUrlFor } : {})}
|
{...(buyUrlFor ? { buyUrlFor } : {})}
|
||||||
{...(resolvedInitialFlightId
|
{...(resolvedInitialFlightId
|
||||||
@@ -364,7 +362,6 @@ export const DayGroupedFlightList: FC<DayGroupedFlightListProps> = ({
|
|||||||
loading={false}
|
loading={false}
|
||||||
direction="schedule"
|
direction="schedule"
|
||||||
renderExpandedBody={renderScheduleBody}
|
renderExpandedBody={renderScheduleBody}
|
||||||
shareUrlFor={(flight) => buildFlightShareUrl(flight, locale, "Schedule")}
|
|
||||||
{...(onFlightClick ? { onFlightClick } : {})}
|
{...(onFlightClick ? { onFlightClick } : {})}
|
||||||
{...(buyUrlFor ? { buyUrlFor } : {})}
|
{...(buyUrlFor ? { buyUrlFor } : {})}
|
||||||
{...(resolvedInitialFlightId
|
{...(resolvedInitialFlightId
|
||||||
|
|||||||
@@ -414,12 +414,11 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
|
|||||||
<FlightActions
|
<FlightActions
|
||||||
flight={miniListCurrentFlight}
|
flight={miniListCurrentFlight}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
viewType="Schedule"
|
|
||||||
showStatus={miniListCurrentFlight.routeType === "Direct"}
|
showStatus={miniListCurrentFlight.routeType === "Direct"}
|
||||||
showRegister={false}
|
showRegister={false}
|
||||||
forceBuy
|
forceBuy
|
||||||
/>
|
/>
|
||||||
<LastUpdate flight={miniListCurrentFlight} locale={locale} viewType="Schedule" />
|
<LastUpdate flight={miniListCurrentFlight} locale={locale} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{miniListCurrentFlight.routeType !== "Direct" && (
|
{miniListCurrentFlight.routeType !== "Direct" && (
|
||||||
|
|||||||
@@ -397,8 +397,6 @@ export const ScheduleFlightBody: FC<ScheduleFlightBodyProps> = ({
|
|||||||
<FlightActions
|
<FlightActions
|
||||||
flight={flight}
|
flight={flight}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
viewType="Schedule"
|
|
||||||
showShare
|
|
||||||
showBuy
|
showBuy
|
||||||
showRegister
|
showRegister
|
||||||
showStatus
|
showStatus
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* and is labelled with the table + column reference for traceability.
|
* and is labelled with the table + column reference for traceability.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { FlightCard } from "./FlightCard.js";
|
import { FlightCard } from "./FlightCard.js";
|
||||||
import type { ISimpleFlight } from "@/features/online-board/types.js";
|
import type { ISimpleFlight } from "@/features/online-board/types.js";
|
||||||
|
|
||||||
@@ -623,35 +623,9 @@ describe("4.1.13.4 — Expanded row content (TZ §4.1.13.4.3)", () => {
|
|||||||
expect(screen.getByTestId("transition-gate").textContent).toContain("C7");
|
expect(screen.getByTestId("transition-gate").textContent).toContain("C7");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("4.1.13.4-R: share button always present in expanded actions row", () => {
|
it("4.1.13.4-R: share button is not rendered in expanded actions row", () => {
|
||||||
render(<FlightCard flight={makeFlight({})} expandable initialExpanded onViewDetails={() => {}} />);
|
render(<FlightCard flight={makeFlight({})} expandable initialExpanded onViewDetails={() => {}} />);
|
||||||
expect(screen.getByTestId("flight-share-button")).toBeTruthy();
|
expect(screen.queryByTestId("flight-share-button")).toBeNull();
|
||||||
});
|
|
||||||
|
|
||||||
it("4.1.13.4-R: share button copies caller-provided details URL", async () => {
|
|
||||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
||||||
Object.defineProperty(navigator, "clipboard", {
|
|
||||||
value: { writeText },
|
|
||||||
writable: true,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
render(
|
|
||||||
<FlightCard
|
|
||||||
flight={makeFlight({})}
|
|
||||||
expandable
|
|
||||||
initialExpanded
|
|
||||||
onViewDetails={() => {}}
|
|
||||||
shareUrl="https://example.test/ru-ru/onlineboard/SU0022-20260417"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("flight-share-button"));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(writeText).toHaveBeenCalledWith(
|
|
||||||
"https://example.test/ru-ru/onlineboard/SU0022-20260417",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("4.1.13.4-R: Details button present in expanded actions row", () => {
|
it("4.1.13.4-R: Details button present in expanded actions row", () => {
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ export interface FlightCardProps {
|
|||||||
* buttons Angular shows on the search results expansion.
|
* buttons Angular shows on the search results expansion.
|
||||||
*/
|
*/
|
||||||
renderActions?: (flight: ISimpleFlight) => ReactNode;
|
renderActions?: (flight: ISimpleFlight) => ReactNode;
|
||||||
/** Absolute URL copied/shared by the row share button. */
|
|
||||||
shareUrl?: string;
|
|
||||||
/**
|
/**
|
||||||
* Aeroflot booking URL for the per-row "Купить билет" hover link
|
* Aeroflot booking URL for the per-row "Купить билет" hover link
|
||||||
* (TIRREDESIGN-6 / TZ §4.1.14.4.4). Rendered inside the collapsed row
|
* (TIRREDESIGN-6 / TZ §4.1.14.4.4). Rendered inside the collapsed row
|
||||||
@@ -163,7 +161,6 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
direction = "route",
|
direction = "route",
|
||||||
renderExpandedBody,
|
renderExpandedBody,
|
||||||
renderActions,
|
renderActions,
|
||||||
shareUrl,
|
|
||||||
inlineBuyUrl,
|
inlineBuyUrl,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -667,30 +664,6 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
|
|
||||||
{onViewDetails && (
|
{onViewDetails && (
|
||||||
<div className="flight-card__actions">
|
<div className="flight-card__actions">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flight-card__share-btn"
|
|
||||||
data-testid="flight-share-button"
|
|
||||||
aria-label={t("BOARD.SHARE")}
|
|
||||||
title={t("BOARD.SHARE")}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const url = shareUrl ?? (typeof window !== "undefined"
|
|
||||||
? window.location.href
|
|
||||||
: "");
|
|
||||||
const navShare = (
|
|
||||||
typeof navigator !== "undefined" &&
|
|
||||||
(navigator as Navigator & { share?: (data: ShareData) => Promise<void> }).share
|
|
||||||
);
|
|
||||||
if (navShare && url) {
|
|
||||||
void navShare.call(navigator, { url });
|
|
||||||
} else if (url && typeof navigator !== "undefined" && navigator.clipboard) {
|
|
||||||
void navigator.clipboard.writeText(url);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src="/assets/img/share.svg" alt="" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
{/* Extra actions (Купить / Онлайн регистрация). Angular
|
{/* Extra actions (Купить / Онлайн регистрация). Angular
|
||||||
renders Buy + Registration + Details here on every
|
renders Buy + Registration + Details here on every
|
||||||
expanded board row; the caller wires up FlightActions
|
expanded board row; the caller wires up FlightActions
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ export interface FlightListProps {
|
|||||||
* Купить / Онлайн регистрация alongside Детали рейса.
|
* Купить / Онлайн регистрация alongside Детали рейса.
|
||||||
*/
|
*/
|
||||||
renderActions?: (flight: ISimpleFlight) => ReactNode;
|
renderActions?: (flight: ISimpleFlight) => ReactNode;
|
||||||
/**
|
|
||||||
* Absolute URL for each row-level share button. Callers provide this so
|
|
||||||
* feature-specific route semantics stay outside shared card rendering.
|
|
||||||
*/
|
|
||||||
shareUrlFor?: (flight: ISimpleFlight) => string;
|
|
||||||
/**
|
/**
|
||||||
* Aeroflot booking URL per flight for the hover-reveal inline
|
* Aeroflot booking URL per flight for the hover-reveal inline
|
||||||
* "Купить билет" link (TIRREDESIGN-6). Return null to hide.
|
* "Купить билет" link (TIRREDESIGN-6). Return null to hide.
|
||||||
@@ -67,7 +62,6 @@ export const FlightList: FC<FlightListProps> = ({
|
|||||||
direction = "route",
|
direction = "route",
|
||||||
renderExpandedBody,
|
renderExpandedBody,
|
||||||
renderActions,
|
renderActions,
|
||||||
shareUrlFor,
|
|
||||||
buyUrlFor,
|
buyUrlFor,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -138,10 +132,6 @@ export const FlightList: FC<FlightListProps> = ({
|
|||||||
: {})}
|
: {})}
|
||||||
{...(renderExpandedBody ? { renderExpandedBody } : {})}
|
{...(renderExpandedBody ? { renderExpandedBody } : {})}
|
||||||
{...(renderActions ? { renderActions } : {})}
|
{...(renderActions ? { renderActions } : {})}
|
||||||
{...(() => {
|
|
||||||
const url = shareUrlFor?.(flight);
|
|
||||||
return url ? { shareUrl: url } : {};
|
|
||||||
})()}
|
|
||||||
{...(() => {
|
{...(() => {
|
||||||
const url = buyUrlFor?.(flight);
|
const url = buyUrlFor?.(flight);
|
||||||
return url ? { inlineBuyUrl: url } : {};
|
return url ? { inlineBuyUrl: url } : {};
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { test, expect } from "./fixtures/console-gate";
|
|
||||||
import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures";
|
|
||||||
import { vvoMjzDetailsUrl } from "./helpers/dates";
|
|
||||||
|
|
||||||
test("TIRREDESIGN-32: schedule share button uses clean details URL", async ({
|
|
||||||
page,
|
|
||||||
consoleMessages,
|
|
||||||
}) => {
|
|
||||||
const detailsUrl = vvoMjzDetailsUrl();
|
|
||||||
const cleanPath = detailsUrl.split("?")[0] ?? detailsUrl;
|
|
||||||
|
|
||||||
await routeScheduleVvoMjzFixtures(page);
|
|
||||||
await page.goto(detailsUrl);
|
|
||||||
await expect(page.getByTestId("schedule-details")).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByTestId("share-button").first().click();
|
|
||||||
const vk = page.getByTestId("share-vk");
|
|
||||||
await expect(vk).toBeVisible();
|
|
||||||
|
|
||||||
const href = await vk.getAttribute("href");
|
|
||||||
expect(href).toBeTruthy();
|
|
||||||
const decoded = decodeURIComponent(href ?? "");
|
|
||||||
expect(decoded).toContain(`${new URL(page.url()).origin}${cleanPath}`);
|
|
||||||
expect(decoded).not.toContain("request=schedule-route");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("TIRREDESIGN-32: China locale shows Weibo-only share panel", async ({
|
|
||||||
page,
|
|
||||||
consoleMessages,
|
|
||||||
}) => {
|
|
||||||
const detailsUrl = vvoMjzDetailsUrl().replace("/ru-ru/", "/cn-zh/");
|
|
||||||
|
|
||||||
await routeScheduleVvoMjzFixtures(page);
|
|
||||||
await page.goto(detailsUrl);
|
|
||||||
await expect(page.getByTestId("schedule-details")).toBeVisible({
|
|
||||||
timeout: 15000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByTestId("share-button").first().click();
|
|
||||||
await expect(page.getByTestId("share-weibo")).toBeVisible();
|
|
||||||
await expect(page.getByTestId("share-facebook")).toHaveCount(0);
|
|
||||||
await expect(page.getByTestId("share-vk")).toHaveCount(0);
|
|
||||||
await expect(page.getByTestId("share-twitter")).toHaveCount(0);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { test, expect } from "./fixtures/console-gate";
|
||||||
|
import {
|
||||||
|
routeOnlineboardRouteFixtures,
|
||||||
|
routeScheduleVvoMjzFixtures,
|
||||||
|
} from "./helpers/api-fixtures";
|
||||||
|
import { formatYmd, vvoMjzDetailsUrl } from "./helpers/dates";
|
||||||
|
import { nextOnlineboardDetailsFixture } from "./helpers/onlineboard-fixtures";
|
||||||
|
|
||||||
|
const FIXTURE_DIR = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"../fixtures/api",
|
||||||
|
);
|
||||||
|
const onlineboardDetails = fs.readFileSync(
|
||||||
|
path.join(FIXTURE_DIR, "onlineboard-details.json"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
test("TIRREDESIGN-32: onlineboard details page does not render share buttons", async ({
|
||||||
|
page,
|
||||||
|
consoleMessages,
|
||||||
|
}) => {
|
||||||
|
const details = nextOnlineboardDetailsFixture(onlineboardDetails);
|
||||||
|
await page.route("**/api/flights/v1.1/ru/onlineboard/details?**", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: details.body,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/ru-ru/onlineboard/SU6951-${details.compactDate}`);
|
||||||
|
await expect(page.getByTestId("board-details-header")).toBeVisible({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
await expect(page.getByTestId("share-button")).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("share-panel")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("TIRREDESIGN-32: schedule details page does not render share buttons", async ({
|
||||||
|
page,
|
||||||
|
consoleMessages,
|
||||||
|
}) => {
|
||||||
|
await routeScheduleVvoMjzFixtures(page);
|
||||||
|
await page.goto(vvoMjzDetailsUrl());
|
||||||
|
await expect(page.getByTestId("schedule-details")).toBeVisible({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
await expect(page.getByTestId("share-button")).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("share-panel")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("TIRREDESIGN-32: expanded flight rows do not render row share buttons", async ({
|
||||||
|
page,
|
||||||
|
consoleMessages,
|
||||||
|
}) => {
|
||||||
|
await routeOnlineboardRouteFixtures(page);
|
||||||
|
await page.goto(`/ru-ru/onlineboard/route/MOW-LED-${formatYmd(new Date())}`);
|
||||||
|
await expect(page.locator(".flight-card").first()).toBeVisible({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.locator(".flight-card--clickable").first().click();
|
||||||
|
await expect(page.locator(".flight-card--expanded").first()).toBeVisible();
|
||||||
|
await expect(page.getByTestId("flight-share-button")).toHaveCount(0);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user