Skip to content

Commit 8e6a9be

Browse files
committed
Fix CI
1 parent be59310 commit 8e6a9be

27 files changed

Lines changed: 4433 additions & 436 deletions

File tree

frontend/src/components/async-select/index.test.tsx

Lines changed: 274 additions & 166 deletions
Large diffs are not rendered by default.
Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,108 @@
1-
import { QueryClient } from "@tanstack/react-query";
2-
import { render } from "@testing-library/react";
3-
import { test } from "vitest";
1+
import { act, render, waitFor } from "@testing-library/react";
2+
import { describe, expect, it, vi } from "vitest";
43

5-
import { AsyncTransfer } from "@/components/async-transfer";
6-
import { TestProviders } from "@/providers";
4+
import { AsyncTransfer } from "./index";
75

8-
test("Renders AsyncTransfer", () => {
9-
const queryClient = new QueryClient();
6+
const { mockUseQuery, mockUseIsMobile, transferPropsRef } = vi.hoisted(() => ({
7+
mockUseQuery: vi.fn(),
8+
mockUseIsMobile: vi.fn(),
9+
transferPropsRef: { current: undefined as any },
10+
}));
1011

11-
const onChange = () => undefined;
12-
render(
13-
<TestProviders client={queryClient}>
12+
vi.mock("@tanstack/react-query", () => ({
13+
useQuery: (options: unknown) => mockUseQuery(options),
14+
}));
15+
16+
vi.mock("lodash.debounce", () => ({
17+
default: (fn: (...args: any[]) => any) => fn,
18+
}));
19+
20+
vi.mock("@/hooks/useIsMobile", () => ({
21+
useIsMobile: () => mockUseIsMobile(),
22+
}));
23+
24+
vi.mock("antd", () => ({
25+
Transfer: (props: any) => {
26+
transferPropsRef.current = props;
27+
return <div data-testid="transfer">{props.dataSource.length}</div>;
28+
},
29+
}));
30+
31+
describe("AsyncTransfer", () => {
32+
it("maps datasource, filters and updates search only for left direction", async () => {
33+
mockUseIsMobile.mockReturnValue(false);
34+
mockUseQuery.mockReturnValue({
35+
data: {
36+
results: [
37+
{ id: "1", name: "Alpha" },
38+
{ id: "2", title: "Beta" },
39+
],
40+
},
41+
});
42+
43+
render(
1444
<AsyncTransfer
1545
idField="id"
16-
labelFields={["id"]}
17-
parentModel="test"
18-
onChange={onChange}
46+
labelFields={["name", "title"]}
47+
parentModel="users"
48+
onChange={vi.fn()}
49+
value={["1"]}
50+
layout="vertical"
51+
/>,
52+
);
53+
54+
expect(transferPropsRef.current.dataSource).toEqual([
55+
{ key: "1", title: "Alpha" },
56+
{ key: "2", title: "Beta" },
57+
]);
58+
expect(transferPropsRef.current.render({ title: "X" })).toBe("X");
59+
expect(transferPropsRef.current.filterOption("alp", { key: "Alpha" })).toBe(
60+
true,
61+
);
62+
expect(
63+
transferPropsRef.current.filterOption("bet", { key: "z", value: "Beta" }),
64+
).toBe(true);
65+
expect(
66+
transferPropsRef.current.filterOption("qqq", { key: "a", value: "b" }),
67+
).toBe(false);
68+
expect(transferPropsRef.current.listStyle).toEqual({
69+
width: "100%",
70+
marginTop: 5,
71+
marginBottom: 5,
72+
});
73+
expect(transferPropsRef.current.style).toEqual({ display: "block" });
74+
75+
act(() => {
76+
transferPropsRef.current.onSearch("right", "should-not-apply");
77+
});
78+
let lastQuery = mockUseQuery.mock.calls.at(-1)?.[0] as any;
79+
expect(lastQuery.queryKey[1]).not.toContain("should-not-apply");
80+
81+
act(() => {
82+
transferPropsRef.current.onSearch("left", "john");
83+
});
84+
await waitFor(() => {
85+
lastQuery = mockUseQuery.mock.calls.at(-1)?.[0] as any;
86+
expect(lastQuery.queryKey[1]).toContain("search=john");
87+
});
88+
});
89+
90+
it("uses non-mobile horizontal styles", () => {
91+
mockUseIsMobile.mockReturnValue(false);
92+
mockUseQuery.mockReturnValue({ data: { results: [] } });
93+
94+
render(
95+
<AsyncTransfer
96+
idField="id"
97+
labelFields={["name"]}
98+
parentModel="users"
99+
onChange={vi.fn()}
19100
value={undefined}
20-
/>
21-
</TestProviders>,
22-
);
101+
layout="horizontal"
102+
/>,
103+
);
104+
105+
expect(transferPropsRef.current.listStyle).toEqual({ width: "100%" });
106+
expect(transferPropsRef.current.style).toBeUndefined();
107+
});
23108
});
Lines changed: 244 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,246 @@
1-
import { QueryClient } from "@tanstack/react-query";
2-
import { render } from "@testing-library/react";
3-
import { test } from "vitest";
4-
5-
import { CrudContainer } from "@/components/crud-container";
6-
import { TestProviders } from "@/providers";
7-
8-
test("Renders CrudContainer", () => {
9-
const queryClient = new QueryClient();
10-
render(
11-
<TestProviders client={queryClient}>
12-
<CrudContainer title="test">
13-
<div />
14-
</CrudContainer>
15-
</TestProviders>,
1+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2+
import type React from "react";
3+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4+
5+
import { ROUTES } from "@/constants/routes";
6+
import { ConfigurationContext } from "@/providers/ConfigurationProvider";
7+
import { SignInUserContext } from "@/providers/SignInUserProvider";
8+
import { CrudContainer } from "./index";
9+
10+
const {
11+
mockNavigate,
12+
mockUseMutation,
13+
mockMutateSignOut,
14+
mockSignedInUserRefetch,
15+
mockUseIsMobile,
16+
menuPropsRef,
17+
} = vi.hoisted(() => ({
18+
mockNavigate: vi.fn(),
19+
mockUseMutation: vi.fn(),
20+
mockMutateSignOut: vi.fn(),
21+
mockSignedInUserRefetch: vi.fn(),
22+
mockUseIsMobile: vi.fn(),
23+
menuPropsRef: { current: [] as any[] },
24+
}));
25+
26+
vi.mock("@tanstack/react-query", () => ({
27+
useMutation: (options: unknown) => mockUseMutation(options),
28+
}));
29+
30+
vi.mock("react-router-dom", async () => {
31+
const actual =
32+
await vi.importActual<typeof import("react-router-dom")>(
33+
"react-router-dom",
34+
);
35+
return {
36+
...actual,
37+
Link: ({ to, children }: { to: string; children: React.ReactNode }) => (
38+
<a href={to}>{children}</a>
39+
),
40+
useNavigate: () => mockNavigate,
41+
useParams: () => ({ model: "users" }),
42+
};
43+
});
44+
45+
vi.mock("react-helmet-async", () => ({
46+
Helmet: ({ children }: { children: React.ReactNode }) => <>{children}</>,
47+
}));
48+
49+
vi.mock("@/hooks/useIsMobile", () => ({
50+
useIsMobile: () => mockUseIsMobile(),
51+
}));
52+
53+
vi.mock("@/fetchers/fetchers", () => ({
54+
postFetcher: vi.fn(),
55+
}));
56+
57+
vi.mock("@/helpers/title", () => ({
58+
getTitleFromModel: (m: { name: string }) => m.name.toUpperCase(),
59+
}));
60+
61+
vi.mock("react-i18next", async () => {
62+
const actual =
63+
await vi.importActual<typeof import("react-i18next")>("react-i18next");
64+
return {
65+
...actual,
66+
useTranslation: () => ({
67+
t: (key: string) => key,
68+
i18n: { changeLanguage: vi.fn() },
69+
}),
70+
};
71+
});
72+
73+
vi.mock("antd", () => ({
74+
theme: {
75+
useToken: () => ({
76+
token: { colorBgContainer: "#fff", colorPrimary: "#000" },
77+
}),
78+
},
79+
Layout: Object.assign(
80+
({ children }: { children: React.ReactNode }) => <div>{children}</div>,
81+
{
82+
Header: ({ children }: { children: React.ReactNode }) => (
83+
<header>{children}</header>
84+
),
85+
Sider: ({ children }: { children: React.ReactNode }) => (
86+
<aside>{children}</aside>
87+
),
88+
},
89+
),
90+
Typography: {
91+
Title: ({ children }: { children: React.ReactNode }) => <h5>{children}</h5>,
92+
},
93+
Row: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
94+
Col: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
95+
Space: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
96+
Image: ({ alt }: { alt: string }) => <img alt={alt} />,
97+
Input: ({
98+
onChange,
99+
value,
100+
}: {
101+
onChange?: (e: { target: { value: string } }) => void;
102+
value?: string;
103+
}) => (
104+
<div>
105+
<span>{value || ""}</span>
106+
<button
107+
type="button"
108+
onClick={() => onChange?.({ target: { value: "use" } })}
109+
>
110+
trigger-search-menu
111+
</button>
112+
</div>
113+
),
114+
Menu: (props: any) => {
115+
menuPropsRef.current.push(props);
116+
return (
117+
<div>
118+
{(props.items || []).map((item: any) => (
119+
<div key={item.key}>
120+
<button
121+
type="button"
122+
onClick={() => props.onClick?.({ key: item.key })}
123+
>
124+
menu-{item.key}
125+
</button>
126+
{(item.children || []).map((child: any) => (
127+
<button
128+
key={child.key}
129+
type="button"
130+
onClick={() => props.onClick?.({ key: child.key })}
131+
>
132+
menu-{child.key}
133+
</button>
134+
))}
135+
</div>
136+
))}
137+
</div>
138+
);
139+
},
140+
Card: ({
141+
title,
142+
children,
143+
}: {
144+
title: React.ReactNode;
145+
children: React.ReactNode;
146+
}) => (
147+
<div>
148+
<div>{title}</div>
149+
{children}
150+
</div>
151+
),
152+
Skeleton: ({ children }: { children: React.ReactNode }) => (
153+
<div>{children}</div>
154+
),
155+
}));
156+
157+
const configurationValue: any = {
158+
configuration: {
159+
site_name: "FastAdmin",
160+
site_header_logo: "/logo.png",
161+
username_field: "username",
162+
models: [
163+
{ name: "users", permissions: [], actions: [], fields: [] },
164+
{ name: "posts", permissions: [], actions: [], fields: [] },
165+
],
166+
dashboard_widgets: [],
167+
},
168+
};
169+
170+
const renderCrud = (signedIn: boolean, isMobile: boolean) => {
171+
mockUseIsMobile.mockReturnValue(isMobile);
172+
return render(
173+
<ConfigurationContext.Provider value={configurationValue}>
174+
<SignInUserContext.Provider
175+
value={
176+
{
177+
signedIn,
178+
signedInUser: { id: "1", username: "bob" },
179+
signedInUserRefetch: mockSignedInUserRefetch,
180+
} as any
181+
}
182+
>
183+
<CrudContainer
184+
title="Users"
185+
viewOnSite="https://example.com"
186+
headerActions={<div>header-actions</div>}
187+
bottomActions={<div>bottom-actions</div>}
188+
>
189+
<div>content</div>
190+
</CrudContainer>
191+
</SignInUserContext.Provider>
192+
</ConfigurationContext.Provider>,
16193
);
194+
};
195+
196+
describe("CrudContainer", () => {
197+
beforeEach(() => {
198+
vi.clearAllMocks();
199+
menuPropsRef.current = [];
200+
mockUseMutation.mockReturnValue({ mutate: mockMutateSignOut });
201+
});
202+
203+
afterEach(() => {
204+
cleanup();
205+
});
206+
207+
it("handles sidebar, right menu actions and search filtering on desktop", () => {
208+
renderCrud(true, false);
209+
210+
fireEvent.click(screen.getByRole("button", { name: "menu-dashboard" }));
211+
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.HOME);
212+
213+
fireEvent.click(screen.getByRole("button", { name: "menu-users" }));
214+
expect(mockNavigate).toHaveBeenCalledWith("/list/users");
215+
216+
fireEvent.click(screen.getByRole("button", { name: "menu-sign-out" }));
217+
expect(mockMutateSignOut).toHaveBeenCalled();
218+
219+
const mutationOptions = mockUseMutation.mock.calls[0][0] as any;
220+
mutationOptions.onSuccess();
221+
expect(mockSignedInUserRefetch).toHaveBeenCalled();
222+
223+
fireEvent.click(
224+
screen.getByRole("button", { name: "trigger-search-menu" }),
225+
);
226+
expect(screen.queryByRole("button", { name: "menu-posts" })).toBeNull();
227+
expect(screen.getByText("header-actions")).toBeTruthy();
228+
expect(screen.getByText("bottom-actions")).toBeTruthy();
229+
expect(screen.getByText("View on site")).toBeTruthy();
230+
});
231+
232+
it("redirects to sign-in when user is not signed in", () => {
233+
renderCrud(false, false);
234+
expect(mockNavigate).toHaveBeenCalledWith(ROUTES.SIGN_IN);
235+
});
236+
237+
it("covers mobile menu and default menu branches", () => {
238+
renderCrud(true, true);
239+
const menus = menuPropsRef.current;
240+
expect(menus.length).toBeGreaterThan(0);
241+
menus[0].onClick({ key: "users" });
242+
expect(mockNavigate).toHaveBeenCalledWith("/list/users");
243+
menus[menus.length - 1].onClick({ key: "unknown" });
244+
expect(mockMutateSignOut).toHaveBeenCalledTimes(0);
245+
});
17246
});

0 commit comments

Comments
 (0)