From 0ddc418cf99be4baf5be4e7d051ed1c9c16056ad Mon Sep 17 00:00:00 2001 From: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:50:36 +0100 Subject: [PATCH] Move ToastContext and utilities to shared components (#33949) * Move ToastContext and utilities to shared components * lint * fix type * cleanup * fix broken test * fix lint * Add more tests for ToastContext * Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../views/dialogs/UserSettingsDialog.tsx | 2 +- .../views/settings/UserProfileSettings.tsx | 3 +- .../settings/UserProfileSettings-test.tsx | 4 +- .../tabs/user/AccountUserSettingsTab-test.tsx | 5 +- .../unit-tests/contexts/ToastContext-test.ts | 35 ---------- .../src/core/utils/ToastContext.test.tsx | 68 +++++++++++++++++++ .../src/core/utils}/ToastContext.tsx | 20 +++--- packages/shared-components/src/index.ts | 1 + 8 files changed, 89 insertions(+), 49 deletions(-) delete mode 100644 apps/web/test/unit-tests/contexts/ToastContext-test.ts create mode 100644 packages/shared-components/src/core/utils/ToastContext.test.tsx rename {apps/web/src/contexts => packages/shared-components/src/core/utils}/ToastContext.tsx (82%) diff --git a/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx b/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx index 9c7f57f2a1..b69e609971 100644 --- a/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/apps/web/src/components/views/dialogs/UserSettingsDialog.tsx @@ -23,6 +23,7 @@ import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock"; import LabsIcon from "@vector-im/compound-design-tokens/assets/web/icons/labs"; import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block"; import HelpIcon from "@vector-im/compound-design-tokens/assets/web/icons/help"; +import { ToastContext, useActiveToast } from "@element-hq/web-shared-components"; import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView"; import { _t, _td } from "../../../languageHandler"; @@ -46,7 +47,6 @@ import { type NonEmptyArray } from "../../../@types/common"; import { SDKContext, type SdkContextClass } from "../../../contexts/SDKContext"; import { useSettingValue } from "../../../hooks/useSettings"; import { NoChange, useEventEmitterAsyncState, type AsyncStateCallbackResult } from "../../../hooks/useEventEmitter"; -import { ToastContext, useActiveToast } from "../../../contexts/ToastContext"; import { EncryptionUserSettingsTab, type State } from "../settings/tabs/user/EncryptionUserSettingsTab"; interface IProps { diff --git a/apps/web/src/components/views/settings/UserProfileSettings.tsx b/apps/web/src/components/views/settings/UserProfileSettings.tsx index 0ecd5d3dfa..29506d8ccb 100644 --- a/apps/web/src/components/views/settings/UserProfileSettings.tsx +++ b/apps/web/src/components/views/settings/UserProfileSettings.tsx @@ -11,14 +11,13 @@ import { logger } from "matrix-js-sdk/src/logger"; import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; import SignOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/sign-out"; -import { Flex } from "@element-hq/web-shared-components"; +import { Flex, useToastContext } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import AvatarSetting from "./AvatarSetting"; import PosthogTrackers from "../../../PosthogTrackers"; import { formatBytes } from "../../../utils/FormattingUtils"; -import { useToastContext } from "../../../contexts/ToastContext"; import InlineSpinner from "../elements/InlineSpinner"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import CopyableText from "../elements/CopyableText"; diff --git a/apps/web/test/unit-tests/components/views/settings/UserProfileSettings-test.tsx b/apps/web/test/unit-tests/components/views/settings/UserProfileSettings-test.tsx index 0b38cc72f3..eea092d7f5 100644 --- a/apps/web/test/unit-tests/components/views/settings/UserProfileSettings-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/UserProfileSettings-test.tsx @@ -12,10 +12,10 @@ import { type MatrixClient, type UploadResponse } from "matrix-js-sdk/src/matrix import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; import { TooltipProvider } from "@vector-im/compound-web"; +import { ToastContext, type ToastRack } from "@element-hq/web-shared-components"; import UserProfileSettings from "../../../../../src/components/views/settings/UserProfileSettings"; import { mkStubRoom, stubClient } from "../../../../test-utils"; -import { ToastContext, type ToastRack } from "../../../../../src/contexts/ToastContext"; import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import Modal from "../../../../../src/Modal"; @@ -72,7 +72,7 @@ const renderProfileSettings = (toastRack: Partial, client: MatrixClie return render( - + diff --git a/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx b/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx index 2cc235cb52..03f8679312 100644 --- a/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx @@ -12,6 +12,7 @@ import { type MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import userEvent from "@testing-library/user-event"; import { type MockedObject } from "jest-mock-vitest-adapter"; +import { ToastContext, ToastRack } from "@element-hq/web-shared-components"; import AccountUserSettingsTab from "../../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab"; import { SdkContextClass, SDKContext } from "../../../../../../../src/contexts/SDKContext"; @@ -54,7 +55,9 @@ describe("", () => { const getComponent = () => ( - + + + ); diff --git a/apps/web/test/unit-tests/contexts/ToastContext-test.ts b/apps/web/test/unit-tests/contexts/ToastContext-test.ts deleted file mode 100644 index 1124dec7a2..0000000000 --- a/apps/web/test/unit-tests/contexts/ToastContext-test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -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 { ToastRack } from "../../../src/contexts/ToastContext"; - -describe("ToastRack", () => { - it("should return a toast once one is displayed", () => { - const toastRack = new ToastRack(); - toastRack.displayToast("Hello, world!"); - - expect(toastRack.getActiveToast()).toBe("Hello, world!"); - }); - - it("calls update callback when a toast is added", () => { - const toastRack = new ToastRack(); - const updateCallbackFn = jest.fn(); - toastRack.setCallback(updateCallbackFn); - toastRack.displayToast("Hello, world!"); - - expect(updateCallbackFn).toHaveBeenCalled(); - }); - - it("removes toast when remove function is called", () => { - const toastRack = new ToastRack(); - const removeFn = toastRack.displayToast("Hello, world!"); - expect(toastRack.getActiveToast()).toBe("Hello, world!"); - removeFn(); - expect(toastRack.getActiveToast()).toBeUndefined(); - }); -}); diff --git a/packages/shared-components/src/core/utils/ToastContext.test.tsx b/packages/shared-components/src/core/utils/ToastContext.test.tsx new file mode 100644 index 0000000000..f4ebe92996 --- /dev/null +++ b/packages/shared-components/src/core/utils/ToastContext.test.tsx @@ -0,0 +1,68 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2024 The Matrix.org Foundation C.I.C. + +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 { describe, it, expect, vitest } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import { Toast } from "@vector-im/compound-web"; +import React, { type JSX } from "react"; + +import { ToastContext, ToastRack, useToastContext } from "./ToastContext"; + +describe("ToastRack", () => { + it("should return a toast once one is displayed", () => { + const toastRack = new ToastRack(); + toastRack.displayToast("Hello, world!"); + + expect(toastRack.getActiveToast()).toBe("Hello, world!"); + }); + + it("calls update callback when a toast is added", () => { + const toastRack = new ToastRack(); + const updateCallbackFn = vitest.fn(); + toastRack.setCallback(updateCallbackFn); + toastRack.displayToast("Hello, world!"); + + expect(updateCallbackFn).toHaveBeenCalled(); + }); + + it("removes toast when remove function is called", () => { + const toastRack = new ToastRack(); + const removeFn = toastRack.displayToast("Hello, world!"); + expect(toastRack.getActiveToast()).toBe("Hello, world!"); + removeFn(); + expect(toastRack.getActiveToast()).toBeUndefined(); + }); +}); + +describe("useToastContext", () => { + it("should fail if there is no context wrapper", () => { + function FailingComponent(): JSX.Element { + useToastContext(); + return

Test

; + } + expect(() => render()).toThrow( + "Component must be wrapped in to use useToastContext", + ); + }); + it("should succeed if there is a context wrapper", async () => { + function HappyComponent(): JSX.Element { + const toaster = useToastContext(); + return <>{toaster.getActiveToast()}; + } + const rack = new ToastRack(); + rack.displayToast(Toast!); + const { getByText } = render( + + + , + ); + await waitFor(() => { + expect(getByText("Toast!")).toBeInViewport(); + }); + }); +}); diff --git a/apps/web/src/contexts/ToastContext.tsx b/packages/shared-components/src/core/utils/ToastContext.tsx similarity index 82% rename from apps/web/src/contexts/ToastContext.tsx rename to packages/shared-components/src/core/utils/ToastContext.tsx index 7382a412dc..4cdf2bc0c4 100644 --- a/apps/web/src/contexts/ToastContext.tsx +++ b/packages/shared-components/src/core/utils/ToastContext.tsx @@ -1,10 +1,10 @@ /* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -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. -*/ + * Copyright 2024 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * 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 ReactNode, createContext, useCallback, useContext, useEffect, useState, useMemo } from "react"; @@ -18,14 +18,18 @@ import { type ReactNode, createContext, useCallback, useContext, useEffect, useS * corner of the app, however the name 'toast' as used in this class refers to the component * of the same name in compound that it is written to manage. */ -export const ToastContext = createContext(null as any); +export const ToastContext = createContext(null); ToastContext.displayName = "ToastContext"; /** * Returns the ToastRack in context in order to display toasts */ export function useToastContext(): ToastRack { - return useContext(ToastContext); + const rack = useContext(ToastContext); + if (!rack) { + throw new Error("Component must be wrapped in to use useToastContext"); + } + return rack; } /** diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index d8ada8a735..9c4106bf14 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -86,6 +86,7 @@ export * from "./core/utils/humanize"; export * from "./core/utils/DateUtils"; export * from "./core/utils/numbers"; export * from "./core/utils/FormattingUtils"; +export * from "./core/utils/ToastContext.tsx"; export * from "./core/i18n/I18nApi"; export * from "./core/utils/linkify"; export type * from "./core/userStatus.ts";