From 48759e21499dd92e58cc6e7c7fc4dd41e6964153 Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Mon, 19 Dec 2022 12:02:37 +0100 Subject: [PATCH 1/6] makes sequence of onboarding tasks static --- .../user-onboarding/UserOnboardingList.tsx | 24 +-- .../user-onboarding/UserOnboardingPage.tsx | 4 +- src/hooks/useUserOnboardingContext.ts | 2 +- src/hooks/useUserOnboardingTasks.ts | 23 ++- src/hooks/useUserOnboardingTasks.ts~ | 158 ++++++++++++++++++ test/hooks/useUserOnboardingTasks-test.tsx | 49 ++++++ 6 files changed, 232 insertions(+), 28 deletions(-) create mode 100644 src/hooks/useUserOnboardingTasks.ts~ create mode 100644 test/hooks/useUserOnboardingTasks-test.tsx diff --git a/src/components/views/user-onboarding/UserOnboardingList.tsx b/src/components/views/user-onboarding/UserOnboardingList.tsx index 29bdd98b41a..1098511ea82 100644 --- a/src/components/views/user-onboarding/UserOnboardingList.tsx +++ b/src/components/views/user-onboarding/UserOnboardingList.tsx @@ -15,9 +15,8 @@ limitations under the License. */ import * as React from "react"; -import { useMemo } from "react"; -import { UserOnboardingTask as Task } from "../../../hooks/useUserOnboardingTasks"; +import { UserOnboardingTaskWithResolvedCompletion } from "../../../hooks/useUserOnboardingTasks"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; import ProgressBar from "../../views/elements/ProgressBar"; @@ -26,23 +25,14 @@ import { UserOnboardingFeedback } from "./UserOnboardingFeedback"; import { UserOnboardingTask } from "./UserOnboardingTask"; interface Props { - completedTasks: Task[]; - waitingTasks: Task[]; + tasks: UserOnboardingTaskWithResolvedCompletion[]; } -export function UserOnboardingList({ completedTasks, waitingTasks }: Props) { - const completed = completedTasks.length; - const waiting = waitingTasks.length; +export function UserOnboardingList({ tasks }: Props) { + const completed = tasks.filter((task) => task.completed === true).length; + const waiting = tasks.filter((task) => task.completed === false).length; const total = completed + waiting; - const tasks = useMemo( - () => [ - ...completedTasks.map((it): [Task, boolean] => [it, true]), - ...waitingTasks.map((it): [Task, boolean] => [it, false]), - ], - [completedTasks, waitingTasks], - ); - return (
@@ -64,8 +54,8 @@ export function UserOnboardingList({ completedTasks, waitingTasks }: Props) { {waiting === 0 && }
    - {tasks.map(([task, completed]) => ( - + {tasks.map((task) => ( + ))}
diff --git a/src/components/views/user-onboarding/UserOnboardingPage.tsx b/src/components/views/user-onboarding/UserOnboardingPage.tsx index 5c0844b6287..a38d792edf0 100644 --- a/src/components/views/user-onboarding/UserOnboardingPage.tsx +++ b/src/components/views/user-onboarding/UserOnboardingPage.tsx @@ -49,7 +49,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props) { const useCase = useSettingValue("FTUE.useCaseSelection"); const context = useUserOnboardingContext(); - const [completedTasks, waitingTasks] = useUserOnboardingTasks(context); + const tasks = useUserOnboardingTasks(context); const initialSyncComplete = useInitialSyncComplete(); const [showList, setShowList] = useState(false); @@ -80,7 +80,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props) { return ( - {showList && } + {showList && } ); } diff --git a/src/hooks/useUserOnboardingContext.ts b/src/hooks/useUserOnboardingContext.ts index 2cf15480d86..cb9471e9138 100644 --- a/src/hooks/useUserOnboardingContext.ts +++ b/src/hooks/useUserOnboardingContext.ts @@ -82,7 +82,7 @@ function useUserOnboardingContextValue(defaultValue: T, callback: (cli: Matri return value; } -export function useUserOnboardingContext(): UserOnboardingContext | null { +export function useUserOnboardingContext(): UserOnboardingContext { const hasAvatar = useUserOnboardingContextValue(false, async (cli) => { const profile = await cli.getProfileInfo(cli.getUserId()); return Boolean(profile?.avatar_url); diff --git a/src/hooks/useUserOnboardingTasks.ts b/src/hooks/useUserOnboardingTasks.ts index bd96961c96a..32ee6a4beb7 100644 --- a/src/hooks/useUserOnboardingTasks.ts +++ b/src/hooks/useUserOnboardingTasks.ts @@ -30,7 +30,7 @@ import { UseCase } from "../settings/enums/UseCase"; import { useSettingValue } from "./useSettings"; import { UserOnboardingContext } from "./useUserOnboardingContext"; -export interface UserOnboardingTask { +interface UserOnboardingTask { id: string; title: string | (() => string); description: string | (() => string); @@ -41,10 +41,11 @@ export interface UserOnboardingTask { href?: string; hideOnComplete?: boolean; }; + completed: (ctx: UserOnboardingContext) => boolean; } -interface InternalUserOnboardingTask extends UserOnboardingTask { - completed: (ctx: UserOnboardingContext) => boolean; +export interface UserOnboardingTaskWithResolvedCompletion extends Omit { + completed: boolean; } const onClickStartDm = (ev: ButtonEvent) => { @@ -52,7 +53,7 @@ const onClickStartDm = (ev: ButtonEvent) => { defaultDispatcher.dispatch({ action: "view_create_chat" }); }; -const tasks: InternalUserOnboardingTask[] = [ +const tasks: UserOnboardingTask[] = [ { id: "create-account", title: _t("Create account"), @@ -143,9 +144,15 @@ const tasks: InternalUserOnboardingTask[] = [ }, ]; -export function useUserOnboardingTasks(context: UserOnboardingContext): [UserOnboardingTask[], UserOnboardingTask[]] { +export function useUserOnboardingTasks(context: UserOnboardingContext) { const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip; - const relevantTasks = useMemo(() => tasks.filter((it) => !it.relevant || it.relevant.includes(useCase)), [useCase]); - const completedTasks = relevantTasks.filter((it) => context && it.completed(context)); - return [completedTasks, relevantTasks.filter((it) => !completedTasks.includes(it))]; + + return useMemo(() => { + return tasks + .filter((task) => !task.relevant || task.relevant.includes(useCase)) + .map((task) => ({ + ...task, + completed: task.completed(context), + })); + }, [context, useCase]); } diff --git a/src/hooks/useUserOnboardingTasks.ts~ b/src/hooks/useUserOnboardingTasks.ts~ new file mode 100644 index 00000000000..d5125fbaf03 --- /dev/null +++ b/src/hooks/useUserOnboardingTasks.ts~ @@ -0,0 +1,158 @@ +/* +Copyright 2022 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 { useMemo } from "react"; + +import { AppDownloadDialog } from "../components/views/dialogs/AppDownloadDialog"; +import { UserTab } from "../components/views/dialogs/UserTab"; +import { ButtonEvent } from "../components/views/elements/AccessibleButton"; +import { Action } from "../dispatcher/actions"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { _t } from "../languageHandler"; +import Modal from "../Modal"; +import { Notifier } from "../Notifier"; +import PosthogTrackers from "../PosthogTrackers"; +import SdkConfig from "../SdkConfig"; +import { UseCase } from "../settings/enums/UseCase"; +import { useSettingValue } from "./useSettings"; +import { UserOnboardingContext } from "./useUserOnboardingContext"; + +interface UserOnboardingTask { + id: string; + title: string | (() => string); + description: string | (() => string); + relevant?: UseCase[]; + action?: { + label: string; + onClick?: (ev?: ButtonEvent) => void; + href?: string; + hideOnComplete?: boolean; + }; + completed: (ctx: UserOnboardingContext) => boolean; +} + +export interface UserOnboardingTaskWithResolvedCompletion extends Omit { + completed: boolean; +} + +const onClickStartDm = (ev: ButtonEvent) => { + PosthogTrackers.trackInteraction("WebUserOnboardingTaskSendDm", ev); + defaultDispatcher.dispatch({ action: "view_create_chat" }); +}; + +const tasks: UserOnboardingTask[] = [ + { + id: "create-account", + title: _t("Create account"), + description: _t("You made it!"), + completed: () => true, + }, + { + id: "find-friends", + title: _t("Find and invite your friends"), + description: _t("It’s what you’re here for, so lets get to it"), + completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, + relevant: [UseCase.PersonalMessaging, UseCase.Skip], + action: { + label: _t("Find friends"), + onClick: onClickStartDm, + }, + }, + { + id: "find-coworkers", + title: _t("Find and invite your co-workers"), + description: _t("Get stuff done by finding your teammates"), + completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, + relevant: [UseCase.WorkMessaging], + action: { + label: _t("Find people"), + onClick: onClickStartDm, + }, + }, + { + id: "find-community-members", + title: _t("Find and invite your community members"), + description: _t("Get stuff done by finding your teammates"), + completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, + relevant: [UseCase.CommunityMessaging], + action: { + label: _t("Find people"), + onClick: onClickStartDm, + }, + }, + { + id: "download-apps", + title: () => + _t("Download %(brand)s", { + brand: SdkConfig.get("brand"), + }), + description: () => + _t("Don’t miss a thing by taking %(brand)s with you", { + brand: SdkConfig.get("brand"), + }), + completed: (ctx: UserOnboardingContext) => ctx.hasDevices, + action: { + label: _t("Download apps"), + onClick: (ev: ButtonEvent) => { + PosthogTrackers.trackInteraction("WebUserOnboardingTaskDownloadApps", ev); + Modal.createDialog(AppDownloadDialog, {}, "mx_AppDownloadDialog_wrapper", false, true); + }, + }, + }, + { + id: "setup-profile", + title: _t("Set up your profile"), + description: _t("Make sure people know it’s really you"), + completed: (ctx: UserOnboardingContext) => ctx.hasAvatar, + action: { + label: _t("Your profile"), + onClick: (ev: ButtonEvent) => { + PosthogTrackers.trackInteraction("WebUserOnboardingTaskSetupProfile", ev); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.General, + }); + }, + }, + }, + { + id: "permission-notifications", + title: _t("Turn on notifications"), + description: _t("Don’t miss a reply or important message"), + completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled, + action: { + label: _t("Enable notifications"), + onClick: (ev: ButtonEvent) => { + PosthogTrackers.trackInteraction("WebUserOnboardingTaskEnableNotifications", ev); + Notifier.setEnabled(true); + }, + hideOnComplete: true, + }, + }, +]; + +export function useUserOnboardingTasks(context: UserOnboardingContext) { + const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip; + + return useMemo(() => { + return tasks + .filter((task) => !task.relevant || task.relevant.includes(useCase)) + .map((task) => ({ + ...task, + completed: task.completed(context), + })) + }, [context, useCase]); +} diff --git a/test/hooks/useUserOnboardingTasks-test.tsx b/test/hooks/useUserOnboardingTasks-test.tsx new file mode 100644 index 00000000000..f2d65382a4d --- /dev/null +++ b/test/hooks/useUserOnboardingTasks-test.tsx @@ -0,0 +1,49 @@ +/* +Copyright 2022 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 { renderHook } from "@testing-library/react-hooks"; + +import { useUserOnboardingTasks } from "../../src/hooks/useUserOnboardingTasks"; + +describe("useUserOnboardingTasks", () => { + it.each([ + { + context: { + hasAvatar: false, + hasDevices: false, + hasDmRooms: false, + hasNotificationsEnabled: false, + }, + }, + { + context: { + hasAvatar: true, + hasDevices: false, + hasDmRooms: false, + hasNotificationsEnabled: true, + }, + }, + ])("sequence should stay static", async ({ context }) => { + const { result } = renderHook(() => useUserOnboardingTasks(context)); + + expect(result.current).toHaveLength(5); + expect(result.current[0].id).toBe("create-account"); + expect(result.current[1].id).toBe("find-friends"); + expect(result.current[2].id).toBe("download-apps"); + expect(result.current[3].id).toBe("setup-profile"); + expect(result.current[4].id).toBe("permission-notifications"); + }); +}); From 8a4fc8073342c35196bedc718a02999c060461bd Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Mon, 19 Dec 2022 12:06:46 +0100 Subject: [PATCH 2/6] removes unwanted file --- src/hooks/useUserOnboardingTasks.ts~ | 158 --------------------------- 1 file changed, 158 deletions(-) delete mode 100644 src/hooks/useUserOnboardingTasks.ts~ diff --git a/src/hooks/useUserOnboardingTasks.ts~ b/src/hooks/useUserOnboardingTasks.ts~ deleted file mode 100644 index d5125fbaf03..00000000000 --- a/src/hooks/useUserOnboardingTasks.ts~ +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2022 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 { useMemo } from "react"; - -import { AppDownloadDialog } from "../components/views/dialogs/AppDownloadDialog"; -import { UserTab } from "../components/views/dialogs/UserTab"; -import { ButtonEvent } from "../components/views/elements/AccessibleButton"; -import { Action } from "../dispatcher/actions"; -import defaultDispatcher from "../dispatcher/dispatcher"; -import { _t } from "../languageHandler"; -import Modal from "../Modal"; -import { Notifier } from "../Notifier"; -import PosthogTrackers from "../PosthogTrackers"; -import SdkConfig from "../SdkConfig"; -import { UseCase } from "../settings/enums/UseCase"; -import { useSettingValue } from "./useSettings"; -import { UserOnboardingContext } from "./useUserOnboardingContext"; - -interface UserOnboardingTask { - id: string; - title: string | (() => string); - description: string | (() => string); - relevant?: UseCase[]; - action?: { - label: string; - onClick?: (ev?: ButtonEvent) => void; - href?: string; - hideOnComplete?: boolean; - }; - completed: (ctx: UserOnboardingContext) => boolean; -} - -export interface UserOnboardingTaskWithResolvedCompletion extends Omit { - completed: boolean; -} - -const onClickStartDm = (ev: ButtonEvent) => { - PosthogTrackers.trackInteraction("WebUserOnboardingTaskSendDm", ev); - defaultDispatcher.dispatch({ action: "view_create_chat" }); -}; - -const tasks: UserOnboardingTask[] = [ - { - id: "create-account", - title: _t("Create account"), - description: _t("You made it!"), - completed: () => true, - }, - { - id: "find-friends", - title: _t("Find and invite your friends"), - description: _t("It’s what you’re here for, so lets get to it"), - completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, - relevant: [UseCase.PersonalMessaging, UseCase.Skip], - action: { - label: _t("Find friends"), - onClick: onClickStartDm, - }, - }, - { - id: "find-coworkers", - title: _t("Find and invite your co-workers"), - description: _t("Get stuff done by finding your teammates"), - completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, - relevant: [UseCase.WorkMessaging], - action: { - label: _t("Find people"), - onClick: onClickStartDm, - }, - }, - { - id: "find-community-members", - title: _t("Find and invite your community members"), - description: _t("Get stuff done by finding your teammates"), - completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms, - relevant: [UseCase.CommunityMessaging], - action: { - label: _t("Find people"), - onClick: onClickStartDm, - }, - }, - { - id: "download-apps", - title: () => - _t("Download %(brand)s", { - brand: SdkConfig.get("brand"), - }), - description: () => - _t("Don’t miss a thing by taking %(brand)s with you", { - brand: SdkConfig.get("brand"), - }), - completed: (ctx: UserOnboardingContext) => ctx.hasDevices, - action: { - label: _t("Download apps"), - onClick: (ev: ButtonEvent) => { - PosthogTrackers.trackInteraction("WebUserOnboardingTaskDownloadApps", ev); - Modal.createDialog(AppDownloadDialog, {}, "mx_AppDownloadDialog_wrapper", false, true); - }, - }, - }, - { - id: "setup-profile", - title: _t("Set up your profile"), - description: _t("Make sure people know it’s really you"), - completed: (ctx: UserOnboardingContext) => ctx.hasAvatar, - action: { - label: _t("Your profile"), - onClick: (ev: ButtonEvent) => { - PosthogTrackers.trackInteraction("WebUserOnboardingTaskSetupProfile", ev); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.General, - }); - }, - }, - }, - { - id: "permission-notifications", - title: _t("Turn on notifications"), - description: _t("Don’t miss a reply or important message"), - completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled, - action: { - label: _t("Enable notifications"), - onClick: (ev: ButtonEvent) => { - PosthogTrackers.trackInteraction("WebUserOnboardingTaskEnableNotifications", ev); - Notifier.setEnabled(true); - }, - hideOnComplete: true, - }, - }, -]; - -export function useUserOnboardingTasks(context: UserOnboardingContext) { - const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip; - - return useMemo(() => { - return tasks - .filter((task) => !task.relevant || task.relevant.includes(useCase)) - .map((task) => ({ - ...task, - completed: task.completed(context), - })) - }, [context, useCase]); -} From dab852818b59878acef2c9bcecf4cf8b489f0f8c Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Mon, 19 Dec 2022 16:49:56 +0100 Subject: [PATCH 3/6] fixes wrong import --- src/components/views/user-onboarding/UserOnboardingTask.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/user-onboarding/UserOnboardingTask.tsx b/src/components/views/user-onboarding/UserOnboardingTask.tsx index b413c86a29b..4f31d17a0d5 100644 --- a/src/components/views/user-onboarding/UserOnboardingTask.tsx +++ b/src/components/views/user-onboarding/UserOnboardingTask.tsx @@ -17,12 +17,12 @@ limitations under the License. import classNames from "classnames"; import * as React from "react"; -import { UserOnboardingTask as Task } from "../../../hooks/useUserOnboardingTasks"; +import { UserOnboardingTaskWithResolvedCompletion } from "../../../hooks/useUserOnboardingTasks"; import AccessibleButton from "../../views/elements/AccessibleButton"; import Heading from "../../views/typography/Heading"; interface Props { - task: Task; + task: UserOnboardingTaskWithResolvedCompletion; completed?: boolean; } From d9476c7441c57909c722f89bf511cd83a6efc999 Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Tue, 20 Dec 2022 10:35:10 +0100 Subject: [PATCH 4/6] adds missing unit test for UserOnboardingList component --- .../UserOnboardingFeedback.tsx | 2 +- .../user-onboarding/UserOnboardingList.tsx | 15 +++- .../user-onboarding/UserOnboardingTask.tsx | 1 + .../UserOnboardingList-test.tsx | 88 +++++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 test/components/views/user-onboarding/UserOnboardingList-test.tsx diff --git a/src/components/views/user-onboarding/UserOnboardingFeedback.tsx b/src/components/views/user-onboarding/UserOnboardingFeedback.tsx index cd4d21acd3a..567880c6861 100644 --- a/src/components/views/user-onboarding/UserOnboardingFeedback.tsx +++ b/src/components/views/user-onboarding/UserOnboardingFeedback.tsx @@ -30,7 +30,7 @@ export function UserOnboardingFeedback() { } return ( -
+
{_t("How are you finding %(brand)s so far?", { diff --git a/src/components/views/user-onboarding/UserOnboardingList.tsx b/src/components/views/user-onboarding/UserOnboardingList.tsx index 1098511ea82..f1675140633 100644 --- a/src/components/views/user-onboarding/UserOnboardingList.tsx +++ b/src/components/views/user-onboarding/UserOnboardingList.tsx @@ -24,14 +24,23 @@ import Heading from "../../views/typography/Heading"; import { UserOnboardingFeedback } from "./UserOnboardingFeedback"; import { UserOnboardingTask } from "./UserOnboardingTask"; +export const getUserOnboardingCounters = (tasks: UserOnboardingTaskWithResolvedCompletion[]) => { + const completed = tasks.filter((task) => task.completed === true).length; + const waiting = tasks.filter((task) => task.completed === false).length; + + return { + completed: completed, + waiting: waiting, + total: completed + waiting, + }; +}; + interface Props { tasks: UserOnboardingTaskWithResolvedCompletion[]; } export function UserOnboardingList({ tasks }: Props) { - const completed = tasks.filter((task) => task.completed === true).length; - const waiting = tasks.filter((task) => task.completed === false).length; - const total = completed + waiting; + const { completed, waiting, total } = getUserOnboardingCounters(tasks); return (
diff --git a/src/components/views/user-onboarding/UserOnboardingTask.tsx b/src/components/views/user-onboarding/UserOnboardingTask.tsx index 4f31d17a0d5..3d8828e7810 100644 --- a/src/components/views/user-onboarding/UserOnboardingTask.tsx +++ b/src/components/views/user-onboarding/UserOnboardingTask.tsx @@ -32,6 +32,7 @@ export function UserOnboardingTask({ task, completed = false }: Props) { return (
  • { + it.each([ + { + tasks: [], + expectation: { + completed: 0, + waiting: 0, + total: 0, + }, + }, + { + tasks: tasks, + expectation: { + completed: 1, + waiting: 1, + total: 2, + }, + }, + ])("should calculate counters correctly", ({ tasks, expectation }) => { + const result = getUserOnboardingCounters(tasks); + expect(result).toStrictEqual(expectation); + }); +}); + +describe("UserOnboardingList", () => { + // This configuration affects rendering of the feedback and needs to be set. + beforeAll(() => { + SdkConfig.put({ + bug_report_endpoint_url: "https://bug_report_endpoint_url.com", + }); + }); + + it("should not display feedback when there are waiting tasks", async () => { + render(); + + expect(await screen.findByText("Only 1 step to go")).toBeVisible(); + expect(await screen.queryByTestId("user-onboarding-feedback")).toBeNull(); + expect(await screen.findAllByTestId("user-onboarding-task")).toHaveLength(2); + }); + + it("should display feedback when all tasks are completed", async () => { + render( ({ ...task, completed: true }))} />); + + expect(await screen.findByText("You did it!")).toBeVisible(); + expect(await screen.findByTestId("user-onboarding-feedback")).toBeInTheDocument(); + expect(await screen.queryAllByTestId("user-onboarding-task")).toHaveLength(2); + }); +}); From cea876bae5cf66d3edb43c17edff0500a769af76 Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Wed, 21 Dec 2022 12:19:59 +0100 Subject: [PATCH 5/6] adds missing unit test for UserOnboardingPage component --- .../user-onboarding/UserOnboardingList.tsx | 2 +- .../UserOnboardingPage-test.tsx | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 test/components/views/user-onboarding/UserOnboardingPage-test.tsx diff --git a/src/components/views/user-onboarding/UserOnboardingList.tsx b/src/components/views/user-onboarding/UserOnboardingList.tsx index f1675140633..0214e6ac291 100644 --- a/src/components/views/user-onboarding/UserOnboardingList.tsx +++ b/src/components/views/user-onboarding/UserOnboardingList.tsx @@ -43,7 +43,7 @@ export function UserOnboardingList({ tasks }: Props) { const { completed, waiting, total } = getUserOnboardingCounters(tasks); return ( -
    +
    {waiting > 0 diff --git a/test/components/views/user-onboarding/UserOnboardingPage-test.tsx b/test/components/views/user-onboarding/UserOnboardingPage-test.tsx new file mode 100644 index 00000000000..bad03d04de7 --- /dev/null +++ b/test/components/views/user-onboarding/UserOnboardingPage-test.tsx @@ -0,0 +1,101 @@ +/* +Copyright 2022 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 { act, render, RenderResult } from "@testing-library/react"; + +import { filterConsole, stubClient } from "../../../test-utils"; +import { UserOnboardingPage } from "../../../../src/components/views/user-onboarding/UserOnboardingPage"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import SdkConfig from "../../../../src/SdkConfig"; + +jest.mock("../../../../src/components/structures/EmbeddedPage", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(({ url }) =>
    {url}
    ), +})); + +jest.mock("../../../../src/components/structures/HomePage", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() =>
    home page
    ), +})); + +describe("UserOnboardingPage", () => { + let restoreConsole: () => void; + + const renderComponent = async (): Promise => { + const renderResult = render(); + await act(async () => { + jest.runAllTimers(); + }); + return renderResult; + }; + + beforeAll(() => { + restoreConsole = filterConsole( + // unrelated for this test + "could not update user onboarding context", + ); + }); + + beforeEach(() => { + stubClient(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + afterAll(() => { + restoreConsole(); + }); + + describe("when the user registered before the cutoff date", () => { + beforeEach(() => { + jest.spyOn(MatrixClientPeg, "userRegisteredAfter").mockReturnValue(false); + }); + + it("should render the home page", async () => { + expect((await renderComponent()).queryByText("home page")).toBeInTheDocument(); + }); + }); + + describe("when the user registered after the cutoff date", () => { + beforeEach(() => { + jest.spyOn(MatrixClientPeg, "userRegisteredAfter").mockReturnValue(true); + }); + + describe("and there is an explicit home page configured", () => { + beforeEach(() => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + embedded_pages: { + home_url: "https://example.com/home", + }, + }); + }); + + it("should render the configured page", async () => { + expect((await renderComponent()).queryByText("https://example.com/home")).toBeInTheDocument(); + }); + }); + + describe("and there is no home page configured", () => { + it("should render the onboarding", async () => { + expect((await renderComponent()).queryByTestId("user-onboarding-list")).toBeInTheDocument(); + }); + }); + }); +}); From a08f10d04de5288dccac3d24958c5f14bd8e7864 Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Mon, 2 Jan 2023 09:56:42 +0100 Subject: [PATCH 6/6] restores mocks in afterEach to prevent test dependencies --- .../components/views/user-onboarding/UserOnboardingPage-test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/test/components/views/user-onboarding/UserOnboardingPage-test.tsx b/test/components/views/user-onboarding/UserOnboardingPage-test.tsx index bad03d04de7..0ff637e2c91 100644 --- a/test/components/views/user-onboarding/UserOnboardingPage-test.tsx +++ b/test/components/views/user-onboarding/UserOnboardingPage-test.tsx @@ -57,6 +57,7 @@ describe("UserOnboardingPage", () => { afterEach(() => { jest.useRealTimers(); + jest.restoreAllMocks(); }); afterAll(() => {