Skip to content

Commit 9a68a9e

Browse files
authored
chore(ui): plugin tests (#797)
1 parent 2a5fab3 commit 9a68a9e

File tree

14 files changed

+1034
-26
lines changed

14 files changed

+1034
-26
lines changed

typescript/ui/__tests__/setup.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IconProps } from "@iconify/react";
22
import "@testing-library/jest-dom";
3+
import { PropsWithChildren } from "react";
34
import { afterEach, vi } from "vitest";
45

56
vi.mock("react-router", () => ({
@@ -14,6 +15,27 @@ vi.mock("@iconify/react", () => ({
1415
),
1516
}));
1617

18+
// Disable framer-motion animations so portals/modals render immediately
19+
vi.mock("framer-motion", async () => {
20+
return {
21+
// No-op presence
22+
AnimatePresence: ({ children }: { children: React.ReactNode }) => (
23+
<>{children}</>
24+
),
25+
// Render motion.* as simple divs
26+
motion: new Proxy(
27+
{},
28+
{
29+
get:
30+
() =>
31+
({ children, ...props }: PropsWithChildren) => (
32+
<div {...props}>{children}</div>
33+
),
34+
},
35+
),
36+
};
37+
});
38+
1739
Object.defineProperty(window, "scrollTo", {
1840
value: vi.fn(),
1941
writable: true,

typescript/ui/__tests__/unit/ChatMessage.test.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ vi.mock("../../src/core/utils/plugins/PluginWrapper.tsx", () => ({
8989
},
9090
}));
9191

92-
vi.mock("../DelayedTooltip.tsx", () => ({
93-
DelayedTooltip: ({ children }: PropsWithChildren) => children,
94-
}));
95-
9692
vi.mock("react-markdown", () => ({
9793
default: ({ children }: PropsWithChildren) => (
9894
<div className="markdown-content">{children}</div>
@@ -179,12 +175,14 @@ describe("ChatMessage", () => {
179175
const clipboardText = await navigator.clipboard.readText();
180176
expect(clipboardText).toBe("Hello, world!");
181177
await waitFor(async () => {
182-
expect(
183-
await screen.findByTestId("chat-message-copy-icon"),
184-
).toHaveAttribute("data-icon", "heroicons:check");
178+
expect(await screen.findByText("heroicons:check"));
185179
});
186-
// Not waiting for the icon to change due to current bug with fake timers and userEvent
187-
// https://github.com/testing-library/user-event/issues/1115
180+
await waitFor(
181+
async () => {
182+
expect(await screen.findByText("heroicons:clipboard"));
183+
},
184+
{ timeout: 5000 },
185+
);
188186
});
189187
});
190188

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, it, beforeEach, vi, expect, Mock } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import { useStore } from "zustand";
4+
import AuthGuard from "../../../../src/plugins/AuthPlugin/components/AuthGuard";
5+
6+
// --- mocks ---
7+
vi.mock("zustand", async (importOriginal) => ({
8+
...(await importOriginal()),
9+
useStore: vi.fn(),
10+
}));
11+
12+
vi.mock("../../../../src/plugins/AuthPlugin/stores/authStore", () => ({
13+
authStore: {},
14+
}));
15+
16+
vi.mock(
17+
"../../../../src/plugins/AuthPlugin/contexts/AuthStoreContext/AuthStoreContextProvider",
18+
() => ({
19+
AuthStoreContextProvider: ({ children }: { children: React.ReactNode }) => (
20+
<div data-testid="auth-store-context">{children}</div>
21+
),
22+
}),
23+
);
24+
25+
vi.mock("../../../../src/plugins/AuthPlugin/components/AuthWatcher", () => ({
26+
AuthWatcher: () => <div data-testid="auth-watcher" />,
27+
}));
28+
29+
let mockPathname = "/";
30+
vi.mock("react-router", async (importOriginal) => {
31+
const actual = await importOriginal<typeof import("react-router")>();
32+
return {
33+
...actual,
34+
useLocation: () => ({ pathname: mockPathname }),
35+
Navigate: ({ to, replace }: { to: string; replace?: boolean }) => (
36+
<div
37+
data-testid="navigate"
38+
data-to={to}
39+
data-replace={replace?.toString()}
40+
/>
41+
),
42+
};
43+
});
44+
45+
describe("AuthGuard", () => {
46+
const useStoreMock = useStore as Mock;
47+
48+
beforeEach(() => {
49+
vi.resetAllMocks();
50+
mockPathname = "/"; // default
51+
});
52+
53+
it("renders children if route is /login regardless of auth state", () => {
54+
mockPathname = "/login";
55+
useStoreMock.mockReturnValue(false);
56+
57+
render(
58+
<AuthGuard>
59+
<div data-testid="child">Login page child</div>
60+
</AuthGuard>,
61+
);
62+
63+
expect(screen.getByTestId("child")).toBeInTheDocument();
64+
expect(screen.queryByTestId("auth-watcher")).not.toBeInTheDocument();
65+
expect(screen.queryByTestId("navigate")).not.toBeInTheDocument();
66+
});
67+
68+
it("wraps children and renders AuthWatcher if authenticated", () => {
69+
mockPathname = "/dashboard";
70+
useStoreMock.mockReturnValue(true);
71+
72+
render(
73+
<AuthGuard>
74+
<div data-testid="child">Dashboard child</div>
75+
</AuthGuard>,
76+
);
77+
78+
expect(screen.getByTestId("child")).toBeInTheDocument();
79+
expect(screen.getByTestId("auth-store-context")).toBeInTheDocument();
80+
expect(screen.getByTestId("auth-watcher")).toBeInTheDocument();
81+
expect(screen.queryByTestId("navigate")).not.toBeInTheDocument();
82+
});
83+
84+
it("renders Navigate to /login if not authenticated", () => {
85+
mockPathname = "/dashboard";
86+
useStoreMock.mockReturnValue(false);
87+
88+
render(
89+
<AuthGuard>
90+
<div data-testid="child">Dashboard child</div>
91+
</AuthGuard>,
92+
);
93+
94+
const nav = screen.getByTestId("navigate");
95+
expect(nav).toHaveAttribute("data-to", "/login");
96+
expect(nav).toHaveAttribute("data-replace", "true");
97+
expect(screen.queryByTestId("child")).not.toBeInTheDocument();
98+
});
99+
});
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { useNavigate } from "react-router";
2+
import { describe, it, vi, beforeEach, afterEach, expect, Mock } from "vitest";
3+
import { useStore } from "zustand";
4+
import { useConversationProperty } from "../../../../src/core/stores/HistoryStore/selectors";
5+
import { AuthWatcher } from "../../../../src/plugins/AuthPlugin/components/AuthWatcher";
6+
import { act, useState } from "react";
7+
import { fireEvent, render, screen } from "@testing-library/react";
8+
9+
vi.mock("zustand", async (importOriginal) => ({
10+
...(await importOriginal()),
11+
useStore: vi.fn(),
12+
}));
13+
14+
vi.mock("../../../../src/core/stores/HistoryStore/selectors", () => ({
15+
useConversationProperty: vi.fn(),
16+
}));
17+
18+
vi.mock("react-router", () => ({
19+
useNavigate: vi.fn(),
20+
}));
21+
22+
describe("AuthWatcher", () => {
23+
let logoutMock: Mock;
24+
let navigateMock: Mock;
25+
let useStoreMock: Mock;
26+
let useConversationPropertyMock: Mock;
27+
28+
beforeEach(() => {
29+
vi.useFakeTimers();
30+
logoutMock = vi.fn();
31+
navigateMock = vi.fn();
32+
useStoreMock = useStore as Mock;
33+
useConversationPropertyMock = useConversationProperty as Mock;
34+
35+
useStoreMock.mockReturnValue({
36+
token: "token-123",
37+
tokenExpiration: Date.now() + 1000, // 1 second in the future
38+
logout: logoutMock,
39+
});
40+
useConversationPropertyMock.mockReturnValue(false);
41+
42+
(useNavigate as Mock).mockReturnValue(navigateMock);
43+
});
44+
45+
afterEach(() => {
46+
vi.clearAllTimers();
47+
vi.resetAllMocks();
48+
vi.useRealTimers();
49+
vi.clearAllMocks();
50+
});
51+
52+
it("does nothing if token is null", () => {
53+
useStoreMock.mockReturnValue({
54+
token: null,
55+
tokenExpiration: null,
56+
logout: logoutMock,
57+
});
58+
59+
render(<AuthWatcher />);
60+
vi.advanceTimersByTime(10000);
61+
expect(logoutMock).not.toHaveBeenCalled();
62+
expect(navigateMock).not.toHaveBeenCalled();
63+
});
64+
65+
it("logs out immediately if token has expired", () => {
66+
const now = Date.now();
67+
useStoreMock.mockReturnValue({
68+
token: "token-123",
69+
tokenExpiration: now - 1000, // expired
70+
logout: logoutMock,
71+
});
72+
useConversationPropertyMock.mockReturnValue(false);
73+
74+
render(<AuthWatcher />);
75+
vi.runAllTimers();
76+
77+
expect(logoutMock).toHaveBeenCalled();
78+
expect(navigateMock).toHaveBeenCalledWith("/login");
79+
});
80+
81+
it("waits for token expiration and then logs out", () => {
82+
const now = Date.now();
83+
useStoreMock.mockReturnValue({
84+
token: "token-123",
85+
tokenExpiration: now + 5000, // expires in 5s
86+
logout: logoutMock,
87+
});
88+
useConversationPropertyMock.mockReturnValue(false);
89+
90+
render(<AuthWatcher />);
91+
vi.advanceTimersByTime(4999);
92+
expect(logoutMock).not.toHaveBeenCalled();
93+
94+
vi.advanceTimersByTime(2); // pass expiration
95+
expect(logoutMock).toHaveBeenCalled();
96+
expect(navigateMock).toHaveBeenCalledWith("/login");
97+
});
98+
99+
it("waits until conversations finish loading before logging out", async () => {
100+
const now = Date.now();
101+
useStoreMock.mockReturnValue({
102+
token: "token-123",
103+
tokenExpiration: now - 1000,
104+
logout: logoutMock,
105+
});
106+
107+
const TestWrapper = () => {
108+
const [loading, setLoading] = useState(true);
109+
useConversationPropertyMock.mockReturnValue(loading);
110+
111+
return (
112+
<>
113+
<AuthWatcher />
114+
<button
115+
onClick={() => {
116+
setLoading(false);
117+
}}
118+
>
119+
finish loading
120+
</button>
121+
</>
122+
);
123+
};
124+
125+
render(<TestWrapper />);
126+
act(() => {
127+
vi.advanceTimersByTime(500);
128+
});
129+
expect(logoutMock).not.toHaveBeenCalled();
130+
131+
fireEvent.click(screen.getByText("finish loading"));
132+
act(() => {
133+
vi.advanceTimersByTime(500);
134+
});
135+
136+
expect(logoutMock).toHaveBeenCalledTimes(1);
137+
expect(navigateMock).toHaveBeenCalledWith("/login");
138+
});
139+
});

0 commit comments

Comments
 (0)