diff --git a/frontend/stories/routes/reset-cross-signing.stories.tsx b/frontend/stories/routes/reset-cross-signing.stories.tsx new file mode 100644 index 000000000..1379c4689 --- /dev/null +++ b/frontend/stories/routes/reset-cross-signing.stories.tsx @@ -0,0 +1,73 @@ +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import type { Meta, StoryObj } from "@storybook/react"; +import { HttpResponse, delay } from "msw"; +import { + mockAllowCrossSigningResetMutation, + mockCurrentViewerQuery, +} from "../../src/gql/graphql"; +import { App } from "./app"; + +const meta = { + title: "Pages/Reset cross signing", + tags: ["!autodocs"], + parameters: { + msw: { + handlers: [ + mockAllowCrossSigningResetMutation(async () => { + await delay(); + + return HttpResponse.json({ + data: { + allowUserCrossSigningReset: { + user: { + id: "user-id", + }, + }, + }, + }); + }), + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Index: Story = { + render: () => , +}; + +export const DeepLink: Story = { + render: () => , +}; + +export const Success: Story = { + render: () => , +}; + +export const Cancelled: Story = { + render: () => , +}; + +export const Errored: Story = { + render: () => , + parameters: { + msw: { + handlers: [ + mockCurrentViewerQuery(() => + HttpResponse.json( + { + errors: [{ message: "Request failed" }], + }, + { status: 400 }, + ), + ), + ], + }, + }, +}; diff --git a/frontend/tests/mocks/handlers.ts b/frontend/tests/mocks/handlers.ts index 1c46dd738..35ba2407d 100644 --- a/frontend/tests/mocks/handlers.ts +++ b/frontend/tests/mocks/handlers.ts @@ -14,6 +14,7 @@ import { CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT } from "../../src/co import { makeFragmentData } from "../../src/gql"; import { mockCurrentUserGreetingQuery, + mockCurrentViewerQuery, mockFooterQuery, mockUserEmailListQuery, mockUserProfileQuery, @@ -41,6 +42,17 @@ export const handlers = [ }), ), + mockCurrentViewerQuery(() => + HttpResponse.json({ + data: { + viewer: { + __typename: "User", + id: "user-id", + }, + }, + }), + ), + mockCurrentUserGreetingQuery(() => HttpResponse.json({ data: { diff --git a/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap b/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap new file mode 100644 index 000000000..2b1cf80d3 --- /dev/null +++ b/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap @@ -0,0 +1,559 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Reset cross signing > renders the cancelled page 1`] = ` + +
+
+
+
+ + + +
+
+

+ Identity reset cancelled. +

+
+
+

+ You can close this window and go back to the app to continue. +

+

+ If you're signed out everywhere and don't remember your recovery code, you'll still need to reset your identity. +

+
+
+
+`; + +exports[`Reset cross signing > renders the deep link page 1`] = ` + +
+
+
+
+ + + +
+
+

+ Reset your identity in case you can't confirm another way +

+
+
+

+ If you're not signed in to any other devices and you've lost your recovery key, then you'll need to reset your identity to continue using the app. +

+
    +
  • + + + +

    + Your account details, contacts, preferences, and chat list will be kept +

    +
  • +
  • + + + + +

    + You will lose any message history that's stored only on the server +

    +
  • +
  • + + + + +

    + You will need to verify all your existing devices and contacts again +

    +
  • +
+

+ Only reset your identity if you don't have access to another signed-in device and you've lost your recovery key. +

+ + + Cancel + +
+ +
+
+`; + +exports[`Reset cross signing > renders the errored page 1`] = ` + +
+
+ + + +
+
+

+ Failed to allow crypto identity reset +

+
+
+

+ This might be a temporary problem, so please try again later. If the problem persists, please contact your server administrator. +

+ +
+`; + +exports[`Reset cross signing > renders the page 1`] = ` + +
+
+
+
+ + + +
+
+

+ Reset your identity in case you can't confirm another way +

+
+
+

+ If you're not signed in to any other devices and you've lost your recovery key, then you'll need to reset your identity to continue using the app. +

+
    +
  • + + + +

    + Your account details, contacts, preferences, and chat list will be kept +

    +
  • +
  • + + + + +

    + You will lose any message history that's stored only on the server +

    +
  • +
  • + + + + +

    + You will need to verify all your existing devices and contacts again +

    +
  • +
+

+ Only reset your identity if you don't have access to another signed-in device and you've lost your recovery key. +

+ + + Back + +
+ +
+
+`; + +exports[`Reset cross signing > renders the success page 1`] = ` + +
+
+
+
+ + + +
+
+

+ Identity reset successfully. Go back to the app to finish the process. +

+
+
+

+ The identity reset has been approved for the next 10 minutes. You can close this window and go back to the app to continue. +

+
+
+
+`; + +exports[`Reset cross signing > renders the success page 2`] = ` + +
+
+
+
+ + + +
+
+

+ Identity reset successfully. Go back to the app to finish the process. +

+
+
+

+ The identity reset has been approved for the next 10 minutes. You can close this window and go back to the app to continue. +

+
+
+
+`; diff --git a/frontend/tests/routes/render.tsx b/frontend/tests/routes/render.tsx index 49a955403..cb3120d55 100644 --- a/frontend/tests/routes/render.tsx +++ b/frontend/tests/routes/render.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -import { QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider, createMemoryHistory } from "@tanstack/react-router"; import { type RenderResult, render } from "@testing-library/react"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -11,9 +11,11 @@ import i18n from "i18next"; import { setupServer } from "msw/node"; import { I18nextProvider } from "react-i18next"; import { afterAll, afterEach, beforeAll } from "vitest"; -import { queryClient } from "../../src/graphql"; -import { router } from "../../src/router"; import { handlers } from "../mocks/handlers"; +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "../../src/routeTree.gen"; + +// Create a new router instance export const server = setupServer(...handlers); @@ -27,21 +29,21 @@ afterAll(() => server.close()); afterEach(() => server.resetHandlers()); async function renderPage(route: string): Promise { - await router.load(); - - const history = createMemoryHistory({ - initialEntries: [route], + // Create a new query client and a new router + const queryClient = new QueryClient(); + const history = createMemoryHistory({ initialEntries: [route] }); + const router = createRouter({ + routeTree, + context: { queryClient }, + history, }); + await router.load(); return render( - + , diff --git a/frontend/tests/routes/reset-cross-signing.test.tsx b/frontend/tests/routes/reset-cross-signing.test.tsx new file mode 100644 index 000000000..9477388bf --- /dev/null +++ b/frontend/tests/routes/reset-cross-signing.test.tsx @@ -0,0 +1,109 @@ +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +// @vitest-environment happy-dom + +import { waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { HttpResponse } from "msw"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + mockAllowCrossSigningResetMutation, + mockCurrentViewerQuery, +} from "../../src/gql/graphql"; +import { renderPage, server } from "./render"; + +afterEach(() => { + window.onAuthDone = undefined; +}); + +describe("Reset cross signing", () => { + it("renders the page", async () => { + const { asFragment } = await renderPage("/reset-cross-signing"); + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders the deep link page", async () => { + const { asFragment } = await renderPage( + "/reset-cross-signing?deepLink=true", + ); + expect(asFragment()).toMatchSnapshot(); + }); + + it("calls the callback on success", async () => { + // TODO: a better way to wait on delays + let advance: () => void; + const wait = new Promise((resolve) => { + advance = () => resolve(void 0); + }); + + window.onAuthDone = vi.fn(); + + server.use( + mockAllowCrossSigningResetMutation(async () => { + await wait; + return HttpResponse.json({ + data: { + allowUserCrossSigningReset: { + user: { + id: "user-id", + }, + }, + }, + }); + }), + ); + + const user = userEvent.setup(); + const { getByRole } = await renderPage( + "/reset-cross-signing?deepLink=true", + ); + + const finishButton = getByRole("button", { name: "Finish reset" }); + + expect(finishButton).not.toHaveAttribute("aria-disabled", "true"); + await user.click(finishButton); + // The button is in a loading state + await waitFor(() => + expect(finishButton).toHaveAttribute("aria-disabled", "true"), + ); + + expect(window.onAuthDone).not.toHaveBeenCalled(); + advance(); + await waitFor(() => expect(finishButton).not.toBeInTheDocument()); + expect(window.onAuthDone).toHaveBeenCalled(); + }); + + it("renders the success page", async () => { + const { asFragment } = await renderPage("/reset-cross-signing/success"); + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders the success page", async () => { + const { asFragment } = await renderPage("/reset-cross-signing/success"); + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders the cancelled page", async () => { + const { asFragment } = await renderPage("/reset-cross-signing/cancelled"); + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders the errored page", async () => { + server.use( + mockCurrentViewerQuery(() => + HttpResponse.json( + { + errors: [{ message: "Request failed" }], + }, + { status: 400 }, + ), + ), + ); + + const { asFragment } = await renderPage("/reset-cross-signing/"); + expect(asFragment()).toMatchSnapshot(); + }); +});