+
{waiting > 0
@@ -64,8 +63,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 5c0844b628..a38d792edf 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/components/views/user-onboarding/UserOnboardingTask.tsx b/src/components/views/user-onboarding/UserOnboardingTask.tsx
index b413c86a29..3d8828e781 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;
}
@@ -32,6 +32,7 @@ export function UserOnboardingTask({ task, completed = false }: Props) {
return (
(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 bd96961c96..32ee6a4beb 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/test/components/views/user-onboarding/UserOnboardingList-test.tsx b/test/components/views/user-onboarding/UserOnboardingList-test.tsx
new file mode 100644
index 0000000000..4c63b391eb
--- /dev/null
+++ b/test/components/views/user-onboarding/UserOnboardingList-test.tsx
@@ -0,0 +1,88 @@
+/*
+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 { screen, render } from "@testing-library/react";
+
+import {
+ getUserOnboardingCounters,
+ UserOnboardingList,
+} from "../../../../src/components/views/user-onboarding/UserOnboardingList";
+import SdkConfig from "../../../../src/SdkConfig";
+
+const tasks = [
+ {
+ id: "1",
+ title: "Lorem ipsum",
+ description: "Lorem ipsum dolor amet.",
+ completed: true,
+ },
+ {
+ id: "2",
+ title: "Lorem ipsum",
+ description: "Lorem ipsum dolor amet.",
+ completed: false,
+ },
+];
+
+describe("getUserOnboardingCounters()", () => {
+ 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);
+ });
+});
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 0000000000..0ff637e2c9
--- /dev/null
+++ b/test/components/views/user-onboarding/UserOnboardingPage-test.tsx
@@ -0,0 +1,102 @@
+/*
+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();
+ jest.restoreAllMocks();
+ });
+
+ 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();
+ });
+ });
+ });
+});
diff --git a/test/hooks/useUserOnboardingTasks-test.tsx b/test/hooks/useUserOnboardingTasks-test.tsx
new file mode 100644
index 0000000000..f2d65382a4
--- /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");
+ });
+});