Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions echo/frontend/src/components/release/ReleaseVideoModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ vi.mock("@/components/layout/TransitionCurtainProvider", () => ({
useTransitionCurtain: () => curtainState,
}));

// Captures land here instead of a network; each test reads what it needs.
const captured: Array<{ event: string; props: Record<string, unknown> }> = [];

vi.mock("@posthog/react", () => ({
usePostHog: () => ({
capture: (event: string, props: Record<string, unknown>) => {
captured.push({ event, props });
},
}),
}));

// A non-default language, so the assertions prove the real one is carried.
vi.mock("@/hooks/useLanguage", () => ({
useLanguage: () => ({ language: "nl-NL" }),
}));

import { ReleaseVideoModal } from "./ReleaseVideoModal";
import { getReleases } from "./releases";
import { RELEASE_VIDEO_SEEN_KEY } from "./releaseVideo";
Expand Down Expand Up @@ -73,6 +89,7 @@ beforeEach(() => {
curtainState.isActive = false;
meState.data = { settings: {} };
meState.isSuccess = true;
captured.length = 0;
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(null, { status: 200 })),
Expand Down Expand Up @@ -260,6 +277,171 @@ describe("opening it on demand", () => {
});
});

// What the analytics must answer: did they watch, how far, in which language,
// from which trigger. The player never loads in jsdom, so the widget messages
// are hand-delivered exactly as the embed would post them.
describe("analytics", () => {
const EMBED_ORIGIN = "https://www.youtube-nocookie.com";

const capturesOf = (event: string) =>
captured.filter((capture) => capture.event === event);

const frameElement = () =>
screen.getByTitle("Release video") as HTMLIFrameElement;

const deliver = (info: Record<string, unknown>) => {
fireEvent(
window,
new MessageEvent("message", {
data: JSON.stringify({ event: "infoDelivery", info }),
origin: EMBED_ORIGIN,
source: frameElement().contentWindow,
}),
);
};

it("records the automatic showing with language, version and trigger", () => {
renderModal();
const opens = capturesOf("whats_new_modal_opened");
expect(opens).toHaveLength(1);
expect(opens[0].props).toMatchObject({
language: "nl-NL",
trigger: "auto",
version: LATEST.version,
});
expect(typeof opens[0].props.seconds_since_page_load).toBe("number");
});

it("marks a sidebar showing as manual", async () => {
meState.data = { settings: { [RELEASE_VIDEO_SEEN_KEY]: LATEST.version } };
const Reopen = () => {
const [requested, handlers] = useDisclosure(false);
return (
<>
<button onClick={handlers.open} type="button">
What's new
</button>
<ReleaseVideoModal
onRequestedClose={handlers.close}
requested={requested}
/>
</>
);
};
render(
<QueryClientProvider client={new QueryClient()}>
<I18nProvider i18n={i18n}>
<MantineProvider>
<Reopen />
</MantineProvider>
</I18nProvider>
</QueryClientProvider>,
);
expect(capturesOf("whats_new_modal_opened")).toHaveLength(0);

fireEvent.click(screen.getByRole("button", { name: "What's new" }));
await waitFor(() => {
expect(capturesOf("whats_new_modal_opened")).toHaveLength(1);
});
expect(capturesOf("whats_new_modal_opened")[0].props.trigger).toBe(
"manual",
);
});

it("turns the embed's own messages into started, progress and completed", () => {
renderModal();
deliver({ currentTime: 0, duration: 100, playerState: 1 });
deliver({ currentTime: 1 });
deliver({ currentTime: 2 });
expect(capturesOf("whats_new_video_started")).toHaveLength(1);

deliver({ currentTime: 30 });
const progress = capturesOf("whats_new_video_progress");
expect(progress).toHaveLength(1);
expect(progress[0].props.milestone_percent).toBe(25);

deliver({ currentTime: 100, playerState: 0 });
expect(capturesOf("whats_new_video_progress")).toHaveLength(4);
const completed = capturesOf("whats_new_video_completed");
expect(completed).toHaveLength(1);
expect(completed[0].props.video_duration_seconds).toBe(100);
});

it("ignores messages that are not from the embed", () => {
renderModal();
fireEvent(
window,
new MessageEvent("message", {
data: JSON.stringify({
event: "infoDelivery",
info: { playerState: 1 },
}),
origin: "https://www.youtube.com",
source: frameElement().contentWindow,
}),
);
fireEvent(
window,
new MessageEvent("message", {
data: JSON.stringify({
event: "infoDelivery",
info: { playerState: 1 },
}),
origin: EMBED_ORIGIN,
source: window,
}),
);
expect(capturesOf("whats_new_video_started")).toHaveLength(0);
});

it("reports an unwatched showing when closed without playing", async () => {
renderModal();
screen.getByLabelText("Close and go to dembrane").click();
await waitFor(() => {
expect(capturesOf("whats_new_modal_closed")).toHaveLength(1);
});
expect(capturesOf("whats_new_modal_closed")[0].props).toMatchObject({
reason: "dismissed",
video_watched: false,
video_watched_seconds: 0,
});
});

it("carries the watch summary on close", async () => {
renderModal();
deliver({ currentTime: 0, duration: 100, playerState: 1 });
deliver({ currentTime: 1 });
deliver({ currentTime: 2 });

screen.getByLabelText("Close and go to dembrane").click();
await waitFor(() => {
expect(capturesOf("whats_new_modal_closed")).toHaveLength(1);
});
const summary = capturesOf("whats_new_modal_closed")[0].props;
expect(summary).toMatchObject({
video_duration_seconds: 100,
video_watched: true,
video_watched_seconds: 2,
});
expect(summary.video_max_percent).toBe(2);
});

it("flushes the summary once when the tab goes, not again on close", async () => {
renderModal();
fireEvent(window, new Event("pagehide"));
expect(capturesOf("whats_new_modal_closed")).toHaveLength(1);
expect(capturesOf("whats_new_modal_closed")[0].props.reason).toBe(
"pagehide",
);

screen.getByLabelText("Close and go to dembrane").click();
await waitFor(() => {
expect(modalIsOpen()).toBe(false);
});
expect(capturesOf("whats_new_modal_closed")).toHaveLength(1);
});
});

describe("typography", () => {
it("renders nothing italic", () => {
const { baseElement } = renderModal();
Expand Down
Loading
Loading