Files
element-web/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.test.tsx
T
rbondesson 83d732d60e Refactor className and children to component properties instead och view model snapshots in shared components (#32711)
* Refactor className? to component property in EncryptionEventView

* Refactor extraClassNames to default react className as component property for DecryptionFailureBodyView

* Refactor className to component property for MessageTimestampView

* Refactor className and children to component properties for ReactionsRowButton

* Refactor className to component property for DisambiguatedProfile

* Refactor className to a component property in DateSeparatorView

* Fix for lint errors and EncryptionEventView unsupported icon color

* EncryptionEventView fix for icon color css specificity/order
2026-03-05 08:36:45 +00:00

239 lines
8.2 KiB
TypeScript

/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { render, screen, fireEvent } from "@test-utils";
import { composeStories } from "@storybook/react-vite";
import React from "react";
import userEvent from "@testing-library/user-event";
import { describe, it, vi, afterEach, expect } from "vitest";
import * as stories from "./MessageTimestampView.stories.tsx";
import {
MessageTimestampView,
type MessageTimestampViewActions,
type MessageTimestampViewSnapshot,
} from "./MessageTimestampView";
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
import { I18nContext } from "../../utils/i18nContext.ts";
import { I18nApi } from "../../index.ts";
const { Default, HasHref } = composeStories(stories);
const renderWithI18n = (ui: React.ReactElement): ReturnType<typeof render> =>
render(ui, {
wrapper: ({ children }) => <I18nContext.Provider value={new I18nApi()}>{children}</I18nContext.Provider>,
});
describe("MessageTimestampView", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("renders the message timestamp in default state", async () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("applies custom className to the timestamp element", async () => {
const vm = new MockViewModel<MessageTimestampViewSnapshot>({
ts: "04:58",
tsSentAt: "Thu, 17 Nov 2022, 4:58:32 pm",
});
renderWithI18n(<MessageTimestampView vm={vm} className="extra_class_1 extra_class_2" />);
const target = screen.getByText("04:58");
expect(target).toHaveClass("extra_class_1", "extra_class_2");
});
it("renders the message timestamp with href", async () => {
const { container } = render(<HasHref />);
expect(container).toMatchSnapshot();
});
const onClick = vi.fn((event: React.MouseEvent<HTMLElement>) => event.preventDefault());
const onContextMenu = vi.fn((event: React.MouseEvent<HTMLElement>) => event.preventDefault());
class MessageTimestampViewModel
extends MockViewModel<MessageTimestampViewSnapshot>
implements MessageTimestampViewActions
{
public onClick = onClick;
public onContextMenu = onContextMenu;
}
it("should attach vm methods with href", async () => {
const vm = new MessageTimestampViewModel({
ts: "04:58",
tsSentAt: "Thu, 17 Nov 2022, 4:58:32 pm",
href: "~",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByRole("link");
fireEvent.click(target);
expect(onClick).toHaveBeenCalled();
fireEvent.contextMenu(target);
expect(onContextMenu).toHaveBeenCalled();
});
it("should attach vm methods without href", async () => {
const user = userEvent.setup();
const vm = new MessageTimestampViewModel({
ts: "04:58",
tsSentAt: "Thu, 17 Nov 2022, 4:58:32 pm",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByRole("link", { hidden: true });
await user.click(target);
expect(onClick).toHaveBeenCalled();
await user.pointer({ target, keys: "[MouseRight]" });
expect(onContextMenu).toHaveBeenCalled();
});
it("should show full date & time on hover", async () => {
const user = userEvent.setup();
const vm = new MessageTimestampViewModel({
ts: "08:09",
tsSentAt: "Fri, Dec 17, 2021, 08:09:00",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
await user.hover(screen.getByRole("link"));
expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Fri, Dec 17, 2021, 08:09:00"`);
});
it("should show sent & received time on hover if passed", async () => {
const user = userEvent.setup();
const vm = new MessageTimestampViewModel({
ts: "08:09",
tsSentAt: "Fri, Dec 17, 2021, 08:09:00",
tsReceivedAt: "Received at: Sat, Dec 18, 2021, 08:09:00",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
await user.hover(screen.getByRole("link"));
expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(
`"Sent at: Fri, Dec 17, 2021, 08:09:00Received at: Received at: Sat, Dec 18, 2021, 08:09:00"`,
);
});
it("handles keyboard activation on span when click handler is set", async () => {
const vm = new MessageTimestampViewModel({
ts: "12:34",
tsSentAt: "Mon, Jan 1, 2024, 12:34:00",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByRole("link");
fireEvent.keyDown(target, { key: "Enter" });
fireEvent.keyDown(target, { key: " " });
expect(onClick).toHaveBeenCalledTimes(2);
});
it("ignores other keys when click handler is set", async () => {
const vm = new MessageTimestampViewModel({
ts: "13:14",
tsSentAt: "Tue, Jun 6, 2023, 13:14:00",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByRole("link");
fireEvent.keyDown(target, { key: "Escape" });
expect(onClick).not.toHaveBeenCalled();
});
it("ignores keyboard activation when no click handler is provided", async () => {
const vm = new MessageTimestampViewModelNoActions({
ts: "15:16",
tsSentAt: "Wed, Jul 7, 2021, 15:16:00",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByText("15:16");
fireEvent.keyDown(target, { key: "Enter" });
expect(onClick).not.toHaveBeenCalled();
});
it("does not wrap tooltip labels when received timestamp is empty", async () => {
const user = userEvent.setup();
const vm = new MessageTimestampViewModel({
ts: "09:10",
tsSentAt: "Tue, Feb 2, 2021, 09:10:00",
tsReceivedAt: "",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
await user.hover(screen.getByRole("link"));
expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Tue, Feb 2, 2021, 09:10:00"`);
});
class MessageTimestampViewModelNoActions extends MockViewModel<MessageTimestampViewSnapshot> {}
it("renders without tooltip when inhibited and no click handler is provided", async () => {
const vm = new MessageTimestampViewModelNoActions({
ts: "07:08",
tsSentAt: "Wed, Mar 3, 2021, 07:08:00",
inhibitTooltip: true,
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByText("07:08");
expect(target).not.toHaveAttribute("role");
expect(target).not.toHaveAttribute("tabindex");
expect(screen.queryByRole("tooltip")).toBeNull();
});
it("keeps link semantics when inhibited but click handler exists", async () => {
const vm = new MessageTimestampViewModel({
ts: "11:12",
tsSentAt: "Thu, Apr 4, 2024, 11:12:00",
inhibitTooltip: true,
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByRole("link");
expect(target).toHaveAttribute("tabindex", "0");
expect(screen.queryByRole("tooltip")).toBeNull();
});
it("exposes focusable span when tooltip is enabled without click handler", async () => {
const user = userEvent.setup();
const vm = new MessageTimestampViewModelNoActions({
ts: "03:04",
tsSentAt: "Fri, May 5, 2023, 03:04:00",
});
renderWithI18n(<MessageTimestampView vm={vm} />);
const target = screen.getByText("03:04");
expect(target).toHaveAttribute("tabindex", "0");
expect(target).not.toHaveAttribute("role");
await user.hover(target);
expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Fri, May 5, 2023, 03:04:00"`);
});
});