Disable RTE formatting buttons when the content contains a slash command (#30802)
* Add ability to disable all formatting buttons * Create hook to check if the content contains a slash command * Disable the formatting buttons if the message content contains a slash command * lint * typo
This commit is contained in:
@@ -43,6 +43,7 @@ function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): J
|
|||||||
element="button"
|
element="button"
|
||||||
onClick={onClick as (e: ButtonEvent) => void}
|
onClick={onClick as (e: ButtonEvent) => void}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
disabled={actionState === "disabled"}
|
||||||
className={classNames("mx_FormattingButtons_Button", {
|
className={classNames("mx_FormattingButtons_Button", {
|
||||||
mx_FormattingButtons_active: actionState === "reversed",
|
mx_FormattingButtons_active: actionState === "reversed",
|
||||||
mx_FormattingButtons_Button_hover: actionState === "enabled",
|
mx_FormattingButtons_Button_hover: actionState === "enabled",
|
||||||
@@ -64,55 +65,59 @@ function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): J
|
|||||||
interface FormattingButtonsProps {
|
interface FormattingButtonsProps {
|
||||||
composer: FormattingFunctions;
|
composer: FormattingFunctions;
|
||||||
actionStates: AllActionStates;
|
actionStates: AllActionStates;
|
||||||
|
/**
|
||||||
|
* Whether all buttons should be disabled
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormattingButtons({ composer, actionStates }: FormattingButtonsProps): JSX.Element {
|
export function FormattingButtons({ composer, actionStates, disabled }: FormattingButtonsProps): JSX.Element {
|
||||||
const composerContext = useComposerContext();
|
const composerContext = useComposerContext();
|
||||||
const isInList = actionStates.unorderedList === "reversed" || actionStates.orderedList === "reversed";
|
const isInList = actionStates.unorderedList === "reversed" || actionStates.orderedList === "reversed";
|
||||||
return (
|
return (
|
||||||
<div className="mx_FormattingButtons">
|
<div className="mx_FormattingButtons">
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.bold}
|
actionState={disabled ? "disabled" : actionStates.bold}
|
||||||
label={_t("composer|format_bold")}
|
label={_t("composer|format_bold")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
||||||
onClick={() => composer.bold()}
|
onClick={() => composer.bold()}
|
||||||
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.italic}
|
actionState={disabled ? "disabled" : actionStates.italic}
|
||||||
label={_t("composer|format_italic")}
|
label={_t("composer|format_italic")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
||||||
onClick={() => composer.italic()}
|
onClick={() => composer.italic()}
|
||||||
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.underline}
|
actionState={disabled ? "disabled" : actionStates.underline}
|
||||||
label={_t("composer|format_underline")}
|
label={_t("composer|format_underline")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
||||||
onClick={() => composer.underline()}
|
onClick={() => composer.underline()}
|
||||||
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.strikeThrough}
|
actionState={disabled ? "disabled" : actionStates.strikeThrough}
|
||||||
label={_t("composer|format_strikethrough")}
|
label={_t("composer|format_strikethrough")}
|
||||||
onClick={() => composer.strikeThrough()}
|
onClick={() => composer.strikeThrough()}
|
||||||
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.unorderedList}
|
actionState={disabled ? "disabled" : actionStates.unorderedList}
|
||||||
label={_t("composer|format_unordered_list")}
|
label={_t("composer|format_unordered_list")}
|
||||||
onClick={() => composer.unorderedList()}
|
onClick={() => composer.unorderedList()}
|
||||||
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
|
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.orderedList}
|
actionState={disabled ? "disabled" : actionStates.orderedList}
|
||||||
label={_t("composer|format_ordered_list")}
|
label={_t("composer|format_ordered_list")}
|
||||||
onClick={() => composer.orderedList()}
|
onClick={() => composer.orderedList()}
|
||||||
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
|
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
{isInList && (
|
{isInList && (
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.indent}
|
actionState={disabled ? "disabled" : actionStates.indent}
|
||||||
label={_t("composer|format_increase_indent")}
|
label={_t("composer|format_increase_indent")}
|
||||||
onClick={() => composer.indent()}
|
onClick={() => composer.indent()}
|
||||||
icon={<IndentIcon className="mx_FormattingButtons_Icon" />}
|
icon={<IndentIcon className="mx_FormattingButtons_Icon" />}
|
||||||
@@ -120,33 +125,33 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
|||||||
)}
|
)}
|
||||||
{isInList && (
|
{isInList && (
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.unindent}
|
actionState={disabled ? "disabled" : actionStates.unindent}
|
||||||
label={_t("composer|format_decrease_indent")}
|
label={_t("composer|format_decrease_indent")}
|
||||||
onClick={() => composer.unindent()}
|
onClick={() => composer.unindent()}
|
||||||
icon={<UnIndentIcon className="mx_FormattingButtons_Icon" />}
|
icon={<UnIndentIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.quote}
|
actionState={disabled ? "disabled" : actionStates.quote}
|
||||||
label={_t("action|quote")}
|
label={_t("action|quote")}
|
||||||
onClick={() => composer.quote()}
|
onClick={() => composer.quote()}
|
||||||
icon={<QuoteIcon className="mx_FormattingButtons_Icon" />}
|
icon={<QuoteIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.inlineCode}
|
actionState={disabled ? "disabled" : actionStates.inlineCode}
|
||||||
label={_t("composer|format_inline_code")}
|
label={_t("composer|format_inline_code")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
||||||
onClick={() => composer.inlineCode()}
|
onClick={() => composer.inlineCode()}
|
||||||
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.codeBlock}
|
actionState={disabled ? "disabled" : actionStates.codeBlock}
|
||||||
label={_t("composer|format_code_block")}
|
label={_t("composer|format_code_block")}
|
||||||
onClick={() => composer.codeBlock()}
|
onClick={() => composer.codeBlock()}
|
||||||
icon={<CodeBlockIcon className="mx_FormattingButtons_Icon" />}
|
icon={<CodeBlockIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
actionState={actionStates.link}
|
actionState={disabled ? "disabled" : actionStates.link}
|
||||||
label={_t("composer|format_link")}
|
label={_t("composer|format_link")}
|
||||||
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
||||||
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
|||||||
import { isNotNull } from "../../../../../Typeguards";
|
import { isNotNull } from "../../../../../Typeguards";
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx";
|
import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx";
|
||||||
|
import { useContainsCommand } from "../hooks/useContainsCommand.ts";
|
||||||
|
|
||||||
interface WysiwygComposerProps {
|
interface WysiwygComposerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -83,6 +84,9 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||||||
}
|
}
|
||||||
}, [onChange, messageContent, disabled]);
|
}, [onChange, messageContent, disabled]);
|
||||||
|
|
||||||
|
// Disable formatting buttons if the message content contains a slash command
|
||||||
|
const disableFormatting = useContainsCommand(content, room);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(e: Event): void {
|
function handleClick(e: Event): void {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -124,7 +128,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||||||
handleAtRoomMention={wysiwyg.mentionAtRoom}
|
handleAtRoomMention={wysiwyg.mentionAtRoom}
|
||||||
handleCommand={wysiwyg.command}
|
handleCommand={wysiwyg.command}
|
||||||
/>
|
/>
|
||||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
<FormattingButtons composer={wysiwyg} actionStates={actionStates} disabled={disableFormatting} />
|
||||||
<Editor
|
<Editor
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabled={!isReady}
|
disabled={!isReady}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector 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 { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
|
||||||
|
import CommandProvider from "../../../../../autocomplete/CommandProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook which determines if the given content contains a slash command.
|
||||||
|
* @returns true if the content contains a slash command, false otherwise.
|
||||||
|
* @param content The content to check for commands.
|
||||||
|
* @param room The current room.
|
||||||
|
*/
|
||||||
|
export function useContainsCommand(content: string | null, room: Room | undefined): boolean {
|
||||||
|
const [contentContainsCommands, setContentContainsCommands] = useState(false);
|
||||||
|
const providerRef = useRef<CommandProvider | null>(null);
|
||||||
|
const currentRoomIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!room || !content) {
|
||||||
|
setContentContainsCommands(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or reuse CommandProvider for the current room
|
||||||
|
if (!providerRef.current || currentRoomIdRef.current !== room.roomId) {
|
||||||
|
providerRef.current = new CommandProvider(room);
|
||||||
|
currentRoomIdRef.current = room.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = providerRef.current;
|
||||||
|
provider
|
||||||
|
.getCompletions(content, { start: 0, end: 0 })
|
||||||
|
.then((results) => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
setContentContainsCommands(true);
|
||||||
|
} else {
|
||||||
|
setContentContainsCommands(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// If there's an error getting completions, assume no commands
|
||||||
|
setContentContainsCommands(false);
|
||||||
|
});
|
||||||
|
}, [content, room]);
|
||||||
|
|
||||||
|
return contentContainsCommands;
|
||||||
|
}
|
||||||
+10
@@ -194,4 +194,14 @@ describe("FormattingButtons", () => {
|
|||||||
expect(screen.getByLabelText("Indent increase")).toBeInTheDocument();
|
expect(screen.getByLabelText("Indent increase")).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("Indent decrease")).toBeInTheDocument();
|
expect(screen.getByLabelText("Indent decrease")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Every button should when disabled the component is disabled", () => {
|
||||||
|
renderComponent({ disabled: true });
|
||||||
|
|
||||||
|
Object.values(testCases).forEach((testCase) => {
|
||||||
|
const { label } = testCase;
|
||||||
|
expect(screen.getByLabelText(label)).toHaveClass(classes.disabled);
|
||||||
|
expect(screen.getByLabelText(label)).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+19
@@ -151,6 +151,25 @@ describe("WysiwygComposer", () => {
|
|||||||
// Then it sends a message
|
// Then it sends a message
|
||||||
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
|
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should disable formatting buttons when a slash command is entered", async () => {
|
||||||
|
// When
|
||||||
|
fireEvent.input(screen.getByRole("textbox"), {
|
||||||
|
data: "/rainbow",
|
||||||
|
inputType: "insertText",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then - wait for all buttons to be rendered and have the disabled class
|
||||||
|
await waitFor(() => {
|
||||||
|
const container = screen.getByTestId("WysiwygComposer");
|
||||||
|
const formattingButtons = container.querySelectorAll(".mx_FormattingButtons_Button");
|
||||||
|
expect(formattingButtons.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
formattingButtons.forEach((btn) => {
|
||||||
|
expect(btn).toHaveClass("mx_FormattingButtons_disabled");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Mentions and commands", () => {
|
describe("Mentions and commands", () => {
|
||||||
|
|||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector 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 { renderHook, waitFor } from "jest-matrix-react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { useContainsCommand } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useContainsCommand";
|
||||||
|
import { stubClient } from "../../../../../../test-utils";
|
||||||
|
|
||||||
|
// Mock CommandProvider
|
||||||
|
const mockGetCompletions = jest.fn();
|
||||||
|
jest.mock("../../../../../../../src/autocomplete/CommandProvider", () => {
|
||||||
|
return jest.fn().mockImplementation(() => ({
|
||||||
|
getCompletions: mockGetCompletions,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("useContainsCommand", () => {
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const client = stubClient();
|
||||||
|
room = new Room("!room:example.com", client, "@user:example.com");
|
||||||
|
mockGetCompletions.mockClear();
|
||||||
|
// Default mock to return empty promise
|
||||||
|
mockGetCompletions.mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when content is null", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand(null, room));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when content is empty string", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand("", room));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when content contains a valid command", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([{ type: "command", completion: "/spoiler" }]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand("/spoiler test message", room));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).toHaveBeenCalledWith("/spoiler test message", { start: 0, end: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when content contains no valid commands", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand("/invalidcommand", room));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).toHaveBeenCalledWith("/invalidcommand", { start: 0, end: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for partial command matches", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([
|
||||||
|
{ type: "command", completion: "/spoiler" },
|
||||||
|
{ type: "command", completion: "/shrug" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand("/sp", room));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).toHaveBeenCalledWith("/sp", { start: 0, end: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update when content changes", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(({ content, room }) => useContainsCommand(content, room), {
|
||||||
|
initialProps: { content: "/invalid", room },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change to valid command
|
||||||
|
mockGetCompletions.mockResolvedValue([{ type: "command", completion: "/spoiler" }]);
|
||||||
|
|
||||||
|
rerender({ content: "/spoiler", room });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).toHaveBeenCalledWith("/spoiler", { start: 0, end: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle CommandProvider errors gracefully", async () => {
|
||||||
|
mockGetCompletions.mockRejectedValueOnce(new Error("Provider error"));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand("/test", room));
|
||||||
|
|
||||||
|
// Should remain false even if promise rejects
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for non-command content", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([]); // CommandProvider returns empty for non-commands
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useContainsCommand("regular message", room));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
expect(mockGetCompletions).toHaveBeenCalledWith("regular message", { start: 0, end: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset to false when switching to null content", async () => {
|
||||||
|
mockGetCompletions.mockResolvedValue([{ type: "command", completion: "/spoiler" }]);
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
({ content, room }: { content: string | null; room: Room | undefined }) =>
|
||||||
|
useContainsCommand(content, room),
|
||||||
|
{
|
||||||
|
initialProps: { content: "/spoiler" as string | null, room: room as Room | undefined },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch to null content
|
||||||
|
rerender({ content: null, room });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user