From d4ebb3d5d6aecf2bcd0869727c83f1fb2f7238d3 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Sat, 7 Mar 2026 18:20:07 +0100 Subject: [PATCH] Fix app login rendering tests Problem was introduced with upgrade to react v19. Fixes #787. --- src/App.test.tsx | 134 ++++++++++++++++++++++++++++++++++++++-- src/resources/users.tsx | 3 +- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 143e5b6..5a52ae2 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,16 +1,138 @@ -import { render, screen } from "@testing-library/react"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; import App from "./App"; +import englishMessages from "./i18n/en"; + +// import { prettyDOM } from "@testing-library/dom"; +// import { writeFileSync } from "node:fs"; + +// Keep auth decisions deterministic so each test can choose the logged-in state explicitly. +const mockedAuthProvider = vi.hoisted(() => ({ + checkAuth: vi.fn(), + checkError: vi.fn(), + getPermissions: vi.fn(), + login: vi.fn(), + logout: vi.fn(), +})); + +// React-admin touches multiple data provider methods while booting the admin shell. +const mockedDataProvider = vi.hoisted(() => ({ + create: vi.fn(), + delete: vi.fn(), + deleteMany: vi.fn(), + getList: vi.fn(), + getMany: vi.fn(), + getManyReference: vi.fn(), + getOne: vi.fn(), + update: vi.fn(), + updateMany: vi.fn(), +})); + +// App creates its QueryClient at module scope, so tests clear the shared cache between renders. +const mockedQueryClient = vi.hoisted(() => ({ + current: null as { clear: () => void } | null, +})); + +vi.mock("@tanstack/react-query", async importOriginal => { + const actual = await importOriginal(); + + class TestQueryClient extends actual.QueryClient { + constructor(...args: ConstructorParameters) { + super(...args); + mockedQueryClient.current = this; + } + } + + return { + ...actual, + QueryClient: TestQueryClient, + }; +}); + +vi.mock("react-admin", async importOriginal => { + const actual = await importOriginal(); + + return { + ...actual, + // The custom import route is outside the assertions in this file and is noisy under test. + CustomRoutes: () => null, + }; +}); vi.mock("./synapse/authProvider", () => ({ __esModule: true, - default: { - logout: vi.fn().mockResolvedValue(undefined), - }, + default: mockedAuthProvider, +})); + +vi.mock("./synapse/dataProvider", () => ({ + __esModule: true, + default: mockedDataProvider, })); describe("App", () => { - it("renders", async () => { + beforeEach(() => { + vi.clearAllMocks(); + + mockedAuthProvider.login.mockResolvedValue(undefined); + mockedAuthProvider.logout.mockResolvedValue(undefined); + mockedAuthProvider.checkAuth.mockRejectedValue(undefined); + mockedAuthProvider.checkError.mockResolvedValue(undefined); + mockedAuthProvider.getPermissions.mockResolvedValue(undefined); + + mockedDataProvider.getList.mockResolvedValue({ data: [], total: 0 }); + mockedDataProvider.getManyReference.mockResolvedValue({ data: [], total: 0 }); + mockedDataProvider.getMany.mockResolvedValue({ data: [] }); + mockedDataProvider.getOne.mockResolvedValue({ data: { id: "1" } }); + mockedDataProvider.create.mockResolvedValue({ data: { id: "1" } }); + mockedDataProvider.update.mockResolvedValue({ data: { id: "1" } }); + mockedDataProvider.updateMany.mockResolvedValue({ data: [] }); + mockedDataProvider.delete.mockResolvedValue({ data: { id: "1" } }); + mockedDataProvider.deleteMany.mockResolvedValue({ data: [] }); + }); + + afterEach(() => { + cleanup(); + mockedQueryClient.current?.clear(); + }); + + it("renders the app after successful login", async () => { + mockedAuthProvider.checkAuth.mockResolvedValue(undefined); + mockedDataProvider.getList.mockResolvedValue({ + data: [ + { + id: "@alice:example.com", + displayname: "Alice", + avatar_src: "mxc://example.com/alice", + is_guest: false, + admin: true, + deactivated: false, + locked: false, + erased: false, + creation_ts: "2024-01-01T00:00:00.000Z", + }, + ], + total: 1, + }); + render(); - await screen.findAllByText("Welcome to Synapse-admin"); + + await screen.findByLabelText("Close menu"); + expect(screen.queryByText(englishMessages.synapseadmin.auth.welcome)).toBeNull(); + await screen.findByText("Alice"); + await waitFor(() => expect(mockedAuthProvider.checkAuth).toHaveBeenCalled()); + + //writeFileSync("./logged-in-screen.html", prettyDOM(document.body, 999999, { highlight: false }) ?? ""); + }); + + it("renders login page when not authenticated", async () => { + render(); + await screen.findByText(englishMessages.synapseadmin.auth.welcome); + screen.getByRole("textbox", { name: englishMessages.ra.auth.username }); + screen.getByText(englishMessages.ra.auth.password); + screen.getByRole("textbox", { name: englishMessages.synapseadmin.auth.base_url }); + screen.getByRole("button", { name: englishMessages.ra.auth.sign_in }); + + await waitFor(() => expect(mockedAuthProvider.checkAuth).toHaveBeenCalled()); + + //writeFileSync("./login-screen.html", prettyDOM(document.body, 999999, { highlight: false }) ?? ""); }); }); diff --git a/src/resources/users.tsx b/src/resources/users.tsx index a1fd5aa..3a4ecd4 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -49,7 +49,6 @@ import { useListContext, Identifier, } from "react-admin"; -import { Link } from "react-router-dom"; import AvatarField from "../components/AvatarField"; import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices"; @@ -73,7 +72,7 @@ const UserListActions = () => { -