Files

151 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from "vitest";
import { act, render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MessagePanel } from "../../../src/features/channels/MessagePanel";
import type { ChannelMessage, ChannelSummary } from "../../../src/features/channels/types";
import i18n from "../../../src/i18n";
// Older live WS messages have no id — preserve that compatible runtime shape.
const restMsg: ChannelMessage = {
id: 1,
packetHash: "ph-rest",
channelHash: "ch1",
senderName: "alice",
content: "from rest",
sentAt: 1000,
};
const liveMsgA = {
packetHash: "ph-live-a",
channelHash: "ch1",
senderName: "bob",
content: "live one",
sentAt: 2000,
} as ChannelMessage;
const liveMsgB = {
packetHash: "ph-live-b",
channelHash: "ch1",
senderName: "carol",
content: "live two",
sentAt: 3000,
} as ChannelMessage;
const multiLineMsg: ChannelMessage = {
id: 2,
packetHash: "ph-multiline",
channelHash: "ch1",
senderName: "dave",
content: "🟠\nDWD aktuell: WARNUNG vor GEWITTER\nDi 17:37 - Di 19:00",
sentAt: 4000,
};
vi.mock("../../../src/hooks/useRegion", () => ({ useRegion: () => ({ iatas: undefined, regionKey: "*" }) }));
vi.mock("../../../src/api/client", () => ({
getScopes: vi.fn(async () => []),
getChannelMessagesPage: vi.fn(() =>
Promise.resolve({ items: [restMsg, liveMsgA, liveMsgB, multiLineMsg], nextCursor: null, hasMore: false }),
),
}));
const channel: ChannelSummary = {
id: 1,
name: "Public",
channelHash: "ch1",
lastSeen: 3000,
isHashtag: false,
keyKnown: true,
};
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = vi.fn();
});
describe("MessagePanel row keys", () => {
it("keys rows without React key warnings when live messages lack an id", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(
<QueryClientProvider client={qc}>
<MessagePanel channel={channel} heardCounts={{}} regionKey="*" />
</QueryClientProvider>,
);
await screen.findByText("live two");
expect(screen.getByText("from rest")).toBeInTheDocument();
expect(screen.getByText("live one")).toBeInTheDocument();
const keyWarnings = errorSpy.mock.calls.filter((args) => String(args[0]).includes("key"));
expect(keyWarnings).toEqual([]);
errorSpy.mockRestore();
});
});
describe("MessagePanel multi-line messages", () => {
it("preserves linebreaks in a message body", async () => {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(
<QueryClientProvider client={qc}>
<MessagePanel channel={channel} heardCounts={{}} regionKey="*" />
</QueryClientProvider>,
);
// jsdom doesn't collapse whitespace the way a browser does, so the class is what pins this;
// the normalizer override stops findByText from collapsing the newlines before matching
const body = await screen.findByText(multiLineMsg.content, { normalizer: (s) => s });
expect(body.className).toContain("whitespace-pre-wrap");
expect(body.className).toContain("break-words");
});
});
describe("MessagePanel heard badge", () => {
const mount = () => render(
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<MessagePanel channel={channel} heardCounts={{ "ph-rest": 3, "ph-live-a": 1 }} regionKey="*" />
</QueryClientProvider>,
);
it("labels the repeat count", async () => {
mount();
expect(await screen.findByTitle("Heard 3 times")).toHaveTextContent("×3");
expect(screen.getByTitle("Heard 1 time")).toBeInTheDocument();
});
it("labels the repeat count in French", async () => {
await i18n.changeLanguage("fr");
mount();
expect(await screen.findByTitle("Entendu 3 fois")).toBeInTheDocument();
expect(screen.getByTitle("Entendu 1 fois")).toBeInTheDocument();
});
});
describe("MessagePanel live follow", () => {
it("scrolls only its own list when a live message arrives, not the page", async () => {
const scrollTo = vi.fn();
window.HTMLElement.prototype.scrollTo = scrollTo as unknown as typeof HTMLElement.prototype.scrollTo;
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(
<QueryClientProvider client={qc}>
<MessagePanel channel={channel} heardCounts={{}} regionKey="*" />
</QueryClientProvider>,
);
await screen.findByText("live two");
const live = { ...liveMsgB, packetHash: "ph-live-c", content: "live three", sentAt: 5000 } as ChannelMessage;
act(() => {
qc.setQueryData(["channel-messages", channel.id, "*", ""], (old: { pages: { items: ChannelMessage[] }[] }) => ({
...old,
pages: [{ ...old.pages[0], items: [live, ...old.pages[0].items] }, ...old.pages.slice(1)],
}));
});
await screen.findByText("live three");
// scrollIntoView also scrolls every ancestor up to the document, shoving the whole app shell up
expect(window.HTMLElement.prototype.scrollIntoView).not.toHaveBeenCalled();
expect(scrollTo).toHaveBeenCalled();
});
});