Iterate design of right panel empty state (#12796)

* Add reusable empty state for the right panel

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-07-19 18:17:40 +01:00 committed by GitHub
parent d202295015
commit 0fc1c53a8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 266 additions and 280 deletions

View file

@ -50,7 +50,7 @@ test.describe("FilePanel", () => {
test.describe("render", () => { test.describe("render", () => {
test("should render empty state", async ({ page }) => { test("should render empty state", async ({ page }) => {
// Wait until the information about the empty state is rendered // Wait until the information about the empty state is rendered
await expect(page.locator(".mx_FilePanel_empty")).toBeVisible(); await expect(page.locator(".mx_EmptyState")).toBeVisible();
// Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332 // Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");

View file

@ -35,7 +35,7 @@ test.describe("NotificationPanel", () => {
await page.getByRole("button", { name: "Notifications" }).click(); await page.getByRole("button", { name: "Notifications" }).click();
// Wait until the information about the empty state is rendered // Wait until the information about the empty state is rendered
await expect(page.locator(".mx_NotificationPanel_empty")).toBeVisible(); await expect(page.locator(".mx_EmptyState")).toBeVisible();
// Take a snapshot of RightPanel // Take a snapshot of RightPanel
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");

View file

@ -104,7 +104,7 @@ test.describe("RightPanel", () => {
await page.getByRole("menuitem", { name: "Files" }).click(); await page.getByRole("menuitem", { name: "Files" }).click();
await expect(page.locator(".mx_FilePanel")).toBeVisible(); await expect(page.locator(".mx_FilePanel")).toBeVisible();
await expect(page.locator(".mx_FilePanel_empty")).toBeVisible(); await expect(page.locator(".mx_EmptyState")).toBeVisible();
await page.getByTestId("base-card-back-button").click(); await page.getByTestId("base-card-back-button").click();
await checkRoomSummaryCard(page, ROOM_NAME); await checkRoomSummaryCard(page, ROOM_NAME);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -73,7 +73,6 @@
@import "./structures/_MatrixChat.pcss"; @import "./structures/_MatrixChat.pcss";
@import "./structures/_MessagePanel.pcss"; @import "./structures/_MessagePanel.pcss";
@import "./structures/_NonUrgentToastContainer.pcss"; @import "./structures/_NonUrgentToastContainer.pcss";
@import "./structures/_NotificationPanel.pcss";
@import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_QuickSettingsButton.pcss";
@import "./structures/_RightPanel.pcss"; @import "./structures/_RightPanel.pcss";
@import "./structures/_RoomSearch.pcss"; @import "./structures/_RoomSearch.pcss";
@ -259,6 +258,7 @@
@import "./views/polls/pollHistory/_PollHistory.pcss"; @import "./views/polls/pollHistory/_PollHistory.pcss";
@import "./views/polls/pollHistory/_PollHistoryList.pcss"; @import "./views/polls/pollHistory/_PollHistoryList.pcss";
@import "./views/right_panel/_BaseCard.pcss"; @import "./views/right_panel/_BaseCard.pcss";
@import "./views/right_panel/_EmptyState.pcss";
@import "./views/right_panel/_EncryptionInfo.pcss"; @import "./views/right_panel/_EncryptionInfo.pcss";
@import "./views/right_panel/_PinnedMessagesCard.pcss"; @import "./views/right_panel/_PinnedMessagesCard.pcss";
@import "./views/right_panel/_RightPanelTabs.pcss"; @import "./views/right_panel/_RightPanelTabs.pcss";

View file

@ -102,7 +102,3 @@ limitations under the License.
padding-inline-start: 0; padding-inline-start: 0;
} }
} }
.mx_FilePanel_empty::before {
--maskImage: url("$(res)/img/element-icons/room/files.svg"); /* See: _RightPanel.pcss */
}

View file

@ -1,19 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_NotificationPanel_empty::before {
--maskImage: url("$(res)/img/element-icons/notifications.svg"); /* See: _RightPanel.pcss */
}

View file

@ -72,30 +72,3 @@ limitations under the License.
order: 2; order: 2;
margin: auto; margin: auto;
} }
.mx_RightPanel_empty {
margin-right: -28px;
h2 {
font-weight: 700;
margin: 16px 0;
}
h2,
p {
font: var(--cpd-font-body-md-regular);
}
&::before {
content: "";
display: block;
margin: 11px auto 29px auto;
height: 42px;
width: 42px;
background-color: $header-panel-text-primary-color;
mask-image: var(--maskImage);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}

View file

@ -0,0 +1,45 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_EmptyState {
height: 100%;
box-sizing: border-box;
padding: var(--cpd-space-4x);
text-align: center;
svg {
width: 56px;
height: 56px;
box-sizing: border-box;
border-radius: 8px;
padding: var(--cpd-space-3x);
background-color: $panel-actions;
}
&::before {
/* Bloom using magic numbers directly out of Figma */
content: "";
position: absolute;
z-index: -1;
width: 642px;
height: 775px;
right: -253.77px;
top: 0;
background: radial-gradient(49.95% 49.95% at 50% 50%, rgba(13, 189, 139, 0.12) 0%, rgba(18, 115, 235, 0) 100%);
transform: rotate(-89.69deg);
overflow: hidden;
}
}

View file

@ -106,10 +106,17 @@ limitations under the License.
} }
.mx_RoomView_messagePanel { .mx_RoomView_messagePanel {
/* To avoid the rule from being applied to .mx_ThreadPanel_empty */ &.mx_RoomView_messageListWrapper {
position: initial;
}
.mx_RoomView_messageListWrapper { .mx_RoomView_messageListWrapper {
width: calc(100% + 6px); /* 8px - 2px */ width: calc(100% + 6px); /* 8px - 2px */
} }
.mx_RoomView_empty {
display: contents;
}
} }
.mx_RoomView_MessageList { .mx_RoomView_MessageList {
@ -168,72 +175,6 @@ limitations under the License.
mask-image: url("$(res)/img/element-icons/link.svg"); mask-image: url("$(res)/img/element-icons/link.svg");
} }
.mx_ThreadPanel_empty {
border-radius: 8px;
background: $background;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
bottom: 0;
left: 0;
padding: 20px;
box-sizing: border-box; /* Include padding and border */
width: 100%;
h2 {
color: $primary-content;
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-18px;
margin-top: 24px;
margin-bottom: 10px;
}
p {
font-size: $font-15px;
color: $secondary-content;
margin: 10px 0;
}
button {
border: none;
background: none;
color: $accent;
font-size: $font-15px;
&:hover,
&:active {
text-decoration: underline;
cursor: pointer;
}
}
.mx_ThreadPanel_empty_tip {
font-size: $font-12px;
line-height: $font-15px;
> b {
font-weight: var(--cpd-font-weight-semibold);
}
}
}
.mx_ThreadPanel_largeIcon {
width: 28px;
height: 28px;
padding: 18px;
background: $system;
border-radius: 50%;
&::after {
@mixin ThreadSummaryIcon;
width: inherit;
height: inherit;
}
}
.mx_ContextualMenu_wrapper { .mx_ContextualMenu_wrapper {
.mx_ThreadPanel_Header_FilterOptionItem { .mx_ThreadPanel_Header_FilterOptionItem {
display: flex; display: flex;

View file

@ -28,6 +28,7 @@ import {
TimelineWindow, TimelineWindow,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Icon as FilesIcon } from "@vector-im/compound-design-tokens/icons/files.svg";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import EventIndexPeg from "../../indexing/EventIndexPeg"; import EventIndexPeg from "../../indexing/EventIndexPeg";
@ -40,6 +41,7 @@ import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -255,10 +257,11 @@ class FilePanel extends React.Component<IProps, IState> {
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
const emptyState = ( const emptyState = (
<div className="mx_RightPanel_empty mx_FilePanel_empty"> <EmptyState
<h2>{_t("file_panel|empty_heading")}</h2> Icon={FilesIcon}
<p>{_t("file_panel|empty_description")}</p> title={_t("file_panel|empty_heading")}
</div> description={_t("file_panel|empty_description")}
/>
); );
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId); const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId);

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications.svg";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
@ -26,6 +27,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import Heading from "../views/typography/Heading"; import Heading from "../views/typography/Heading";
import EmptyState from "../views/right_panel/EmptyState";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -57,10 +59,11 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
public render(): React.ReactNode { public render(): React.ReactNode {
const emptyState = ( const emptyState = (
<div className="mx_RightPanel_empty mx_NotificationPanel_empty"> <EmptyState
<h2>{_t("notif_panel|empty_heading")}</h2> Icon={NotificationsIcon}
<p>{_t("notif_panel|empty_description")}</p> title={_t("notif_panel|empty_heading")}
</div> description={_t("notif_panel|empty_description")}
/>
); );
let content: JSX.Element; let content: JSX.Element;

View file

@ -19,6 +19,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix"; import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix";
import { IconButton, Tooltip } from "@vector-im/compound-web"; import { IconButton, Tooltip } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads.svg";
import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg"; import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
import BaseCard from "../views/right_panel/BaseCard"; import BaseCard from "../views/right_panel/BaseCard";
@ -37,6 +38,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import Heading from "../views/typography/Heading"; import Heading from "../views/typography/Heading";
import { clearRoomNotification } from "../../utils/notifications"; import { clearRoomNotification } from "../../utils/notifications";
import EmptyState from "../views/right_panel/EmptyState";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -73,8 +75,7 @@ export const ThreadPanelHeaderFilterOptionItem: React.FC<
export const ThreadPanelHeader: React.FC<{ export const ThreadPanelHeader: React.FC<{
filterOption: ThreadFilterType; filterOption: ThreadFilterType;
setFilterOption: (filterOption: ThreadFilterType) => void; setFilterOption: (filterOption: ThreadFilterType) => void;
empty: boolean; }> = ({ filterOption, setFilterOption }) => {
}> = ({ filterOption, setFilterOption, empty }) => {
const mxClient = useMatrixClientContext(); const mxClient = useMatrixClientContext();
const roomContext = useRoomContext(); const roomContext = useRoomContext();
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
@ -140,86 +141,24 @@ export const ThreadPanelHeader: React.FC<{
<Heading size="4" className="mx_BaseCard_header_title_heading"> <Heading size="4" className="mx_BaseCard_header_title_heading">
{_t("common|threads")} {_t("common|threads")}
</Heading> </Heading>
{!empty && ( <Tooltip label={_t("threads|mark_all_read")}>
<> <IconButton onClick={onMarkAllThreadsReadClick} aria-label={_t("threads|mark_all_read")} size="24px">
<Tooltip label={_t("threads|mark_all_read")}> <MarkAllThreadsReadIcon />
<IconButton </IconButton>
onClick={onMarkAllThreadsReadClick} </Tooltip>
aria-label={_t("threads|mark_all_read")} <div className="mx_ThreadPanel_vertical_separator" />
size="24px" <ContextMenuButton
> className="mx_ThreadPanel_dropdown"
<MarkAllThreadsReadIcon /> ref={button}
</IconButton> isExpanded={menuDisplayed}
</Tooltip> onClick={(ev: ButtonEvent) => {
<div className="mx_ThreadPanel_vertical_separator" /> openMenu();
<ContextMenuButton PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
className="mx_ThreadPanel_dropdown" }}
ref={button} >
isExpanded={menuDisplayed} {`${_t("threads|show_thread_filter")} ${value?.label}`}
onClick={(ev: ButtonEvent) => { </ContextMenuButton>
openMenu(); {contextMenu}
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
}}
>
{`${_t("threads|show_thread_filter")} ${value?.label}`}
</ContextMenuButton>
{contextMenu}
</>
)}
</div>
);
};
interface EmptyThreadIProps {
hasThreads: boolean;
filterOption: ThreadFilterType;
showAllThreadsCallback: () => void;
}
const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, showAllThreadsCallback }) => {
let body: JSX.Element;
if (hasThreads) {
body = (
<>
<p>
{_t("threads|empty_has_threads_tip", {
replyInThread: _t("action|reply_in_thread"),
})}
</p>
<p>
{/* Always display that paragraph to prevent layout shift when hiding the button */}
{filterOption === ThreadFilterType.My ? (
<button onClick={showAllThreadsCallback}>{_t("threads|show_all_threads")}</button>
) : (
<>&nbsp;</>
)}
</p>
</>
);
} else {
body = (
<>
<p>{_t("threads|empty_explainer")}</p>
<p className="mx_ThreadPanel_empty_tip">
{_t(
"threads|empty_tip",
{
replyInThread: _t("action|reply_in_thread"),
},
{
b: (sub) => <b>{sub}</b>,
},
)}
</p>
</>
);
}
return (
<div className="mx_ThreadPanel_empty">
<div className="mx_ThreadPanel_largeIcon" />
<h2>{_t("threads|empty_heading")}</h2>
{body}
</div> </div>
); );
}; };
@ -268,11 +207,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
<BaseCard <BaseCard
hideHeaderButtons hideHeaderButtons
header={ header={
<ThreadPanelHeader hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
filterOption={filterOption}
setFilterOption={setFilterOption}
empty={!hasThreads}
/>
} }
id="thread-panel" id="thread-panel"
className="mx_ThreadPanel" className="mx_ThreadPanel"
@ -295,10 +230,12 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
timelineSet={timelineSet} timelineSet={timelineSet}
showUrlPreview={false} // No URL previews at the threads list level showUrlPreview={false} // No URL previews at the threads list level
empty={ empty={
<EmptyThread <EmptyState
hasThreads={hasThreads} Icon={ThreadsIcon}
filterOption={filterOption} title={_t("threads|empty_title")}
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)} description={_t("threads|empty_description", {
replyInThread: _t("action|reply_in_thread"),
})}
/> />
} }
alwaysShowTimestamps={true} alwaysShowTimestamps={true}

View file

@ -0,0 +1,42 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentType } from "react";
import { Text } from "@vector-im/compound-web";
import { Flex } from "../../utils/Flex";
interface Props {
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
title: string;
description: string;
}
const EmptyState: React.FC<Props> = ({ Icon, title, description }) => {
return (
<Flex className="mx_EmptyState" direction="column" gap="var(--cpd-space-4x)" align="center" justify="center">
<Icon width="32px" height="32px" />
<Text size="lg" weight="semibold">
{title}
</Text>
<Text size="md" weight="regular">
{description}
</Text>
</Flex>
);
};
export default EmptyState;

View file

@ -3193,16 +3193,13 @@
"one": "%(count)s reply", "one": "%(count)s reply",
"other": "%(count)s replies" "other": "%(count)s replies"
}, },
"empty_explainer": "Threads help keep your conversations on-topic and easy to track.", "empty_description": "Use “%(replyInThread)s” when hovering over a message.",
"empty_has_threads_tip": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.", "empty_title": "Threads help keep your conversations on-topic and easy to track.",
"empty_heading": "Keep discussions organised with threads",
"empty_tip": "<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.",
"error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation",
"mark_all_read": "Mark all as read", "mark_all_read": "Mark all as read",
"my_threads": "My threads", "my_threads": "My threads",
"my_threads_description": "Shows all threads you've participated in", "my_threads_description": "Shows all threads you've participated in",
"open_thread": "Open thread", "open_thread": "Open thread",
"show_all_threads": "Show all threads",
"show_thread_filter": "Show:" "show_thread_filter": "Show:"
}, },
"threads_activity_centre": { "threads_activity_centre": {

View file

@ -0,0 +1,58 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
import { screen, render, waitFor } from "@testing-library/react";
import { mocked } from "jest-mock";
import FilePanel from "../../../src/components/structures/FilePanel";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import { stubClient } from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"),
TimelineWindow: jest.fn().mockReturnValue({
load: jest.fn().mockResolvedValue(null),
getEvents: jest.fn().mockReturnValue([]),
canPaginate: jest.fn().mockReturnValue(false),
}),
}));
describe("FilePanel", () => {
beforeEach(() => {
stubClient();
});
it("renders empty state", async () => {
const cli = MatrixClientPeg.safeGet();
const room = new Room("!room:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const timelineSet = new EventTimelineSet(room);
room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet);
mocked(cli.getRoom).mockReturnValue(room);
const { asFragment } = render(
<FilePanel roomId={room.roomId} onClose={jest.fn()} resizeNotifier={new ResizeNotifier()} />,
);
await waitFor(() => {
expect(screen.getByText("No files visible in this room")).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -43,44 +43,21 @@ describe("ThreadPanel", () => {
describe("Header", () => { describe("Header", () => {
it("expect that All filter for ThreadPanelHeader properly renders Show: All threads", () => { it("expect that All filter for ThreadPanelHeader properly renders Show: All threads", () => {
const { asFragment } = render( const { asFragment } = render(
<ThreadPanelHeader <ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>,
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it("expect that My filter for ThreadPanelHeader properly renders Show: My threads", () => { it("expect that My filter for ThreadPanelHeader properly renders Show: My threads", () => {
const { asFragment } = render( const { asFragment } = render(
<ThreadPanelHeader <ThreadPanelHeader filterOption={ThreadFilterType.My} setFilterOption={() => undefined} />,
empty={false}
filterOption={ThreadFilterType.My}
setFilterOption={() => undefined}
/>,
);
expect(asFragment()).toMatchSnapshot();
});
it("matches snapshot when no threads", () => {
const { asFragment } = render(
<ThreadPanelHeader
empty={true}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>,
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => { it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => {
const { container } = render( const { container } = render(
<ThreadPanelHeader <ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>,
); );
const found = container.querySelector(".mx_ThreadPanel_dropdown"); const found = container.querySelector(".mx_ThreadPanel_dropdown");
expect(found).toBeTruthy(); expect(found).toBeTruthy();
@ -91,11 +68,7 @@ describe("ThreadPanel", () => {
it("expect that ThreadPanelHeader has the correct option selected in the context menu", () => { it("expect that ThreadPanelHeader has the correct option selected in the context menu", () => {
const { container } = render( const { container } = render(
<ThreadPanelHeader <ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>,
); );
fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!); fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!);
const found = screen.queryAllByRole("menuitemradio"); const found = screen.queryAllByRole("menuitemradio");
@ -118,11 +91,7 @@ describe("ThreadPanel", () => {
const { container } = render( const { container } = render(
<RoomContext.Provider value={roomContextObject}> <RoomContext.Provider value={roomContextObject}>
<MatrixClientContext.Provider value={mockClient}> <MatrixClientContext.Provider value={mockClient}>
<ThreadPanelHeader <ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
</RoomContext.Provider>, </RoomContext.Provider>,
); );
@ -136,11 +105,7 @@ describe("ThreadPanel", () => {
const mockClient = createTestClient(); const mockClient = createTestClient();
const { container } = render( const { container } = render(
<MatrixClientContext.Provider value={mockClient}> <MatrixClientContext.Provider value={mockClient}>
<ThreadPanelHeader <ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
); );
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));

View file

@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilePanel renders empty state 1`] = `
<DocumentFragment>
<div
class="mx_BaseCard mx_FilePanel"
>
<div
class="mx_BaseCard_header"
>
<div
class="mx_BaseCard_header_spacer"
/>
<button
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"
style="--cpd-icon-button-size: 28px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<div />
</div>
</button>
</div>
<div
class="mx_RoomView_messagePanel mx_RoomView_messageListWrapper"
>
<div
class="mx_RoomView_empty"
>
<div
class="mx_Flex mx_EmptyState"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
>
<div
height="32px"
width="32px"
/>
<p
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
>
No files visible in this room
</p>
<p
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
>
Attach files from chat or just drag and drop them anywhere in a room.
</p>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View file

@ -95,17 +95,3 @@ exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option
</span> </span>
</div> </div>
`; `;
exports[`ThreadPanel Header matches snapshot when no threads 1`] = `
<DocumentFragment>
<div
class="mx_BaseCard_header_title"
>
<h4
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
>
Threads
</h4>
</div>
</DocumentFragment>
`;

View file

@ -274,6 +274,7 @@ export function createTestClient(): MatrixClient {
matrixRTC: createStubMatrixRTC(), matrixRTC: createStubMatrixRTC(),
isFallbackICEServerAllowed: jest.fn().mockReturnValue(false), isFallbackICEServerAllowed: jest.fn().mockReturnValue(false),
getAuthIssuer: jest.fn(), getAuthIssuer: jest.fn(),
getOrCreateFilter: jest.fn(),
} as unknown as MatrixClient; } as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client); client.reEmitter = new ReEmitter(client);