@btst/stack 3.1.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +112 -103
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +112 -103
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +5 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +5 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +99 -88
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +99 -88
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.cjs +12 -3
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.mjs +12 -3
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +6 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +6 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.cjs +5 -5
- package/dist/packages/stack/src/plugins/ai-chat/client/hooks/chat-hooks.mjs +5 -5
- package/dist/packages/stack/src/plugins/blog/client/components/shared/page-wrapper.cjs +3 -2
- package/dist/packages/stack/src/plugins/blog/client/components/shared/page-wrapper.mjs +3 -2
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-card.cjs +81 -52
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-card.mjs +82 -53
- package/dist/packages/stack/src/plugins/blog/client/components/shared/posts-list.cjs +3 -2
- package/dist/packages/stack/src/plugins/blog/client/components/shared/posts-list.mjs +3 -2
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-list-page.internal.cjs +95 -75
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-list-page.internal.mjs +95 -75
- package/dist/packages/stack/src/plugins/cms/client/components/pages/dashboard-page.internal.cjs +16 -3
- package/dist/packages/stack/src/plugins/cms/client/components/pages/dashboard-page.internal.mjs +16 -3
- package/dist/packages/stack/src/plugins/cms/client/components/shared/page-wrapper.cjs +3 -2
- package/dist/packages/stack/src/plugins/cms/client/components/shared/page-wrapper.mjs +3 -2
- package/dist/packages/stack/src/plugins/cms/utils.cjs +6 -2
- package/dist/packages/stack/src/plugins/cms/utils.mjs +6 -3
- package/dist/packages/stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +14 -8
- package/dist/packages/stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +14 -8
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.cjs +94 -87
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.mjs +94 -87
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +81 -74
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +81 -74
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/page-wrapper.cjs +3 -2
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/page-wrapper.mjs +3 -2
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/pagination.cjs +25 -16
- package/dist/packages/stack/src/plugins/form-builder/client/components/shared/pagination.mjs +25 -16
- package/dist/packages/stack/src/plugins/form-builder/utils.cjs +6 -2
- package/dist/packages/stack/src/plugins/form-builder/utils.mjs +6 -3
- package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +7 -5
- package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +7 -5
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +10 -8
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +10 -8
- package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/index.d.ts +2 -2
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +20 -2
- package/dist/plugins/blog/client/index.d.mts +20 -2
- package/dist/plugins/blog/client/index.d.ts +20 -2
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/client/index.d.cts +18 -0
- package/dist/plugins/cms/client/index.d.mts +18 -0
- package/dist/plugins/cms/client/index.d.ts +18 -0
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +16 -0
- package/dist/plugins/form-builder/client/index.d.mts +16 -0
- package/dist/plugins/form-builder/client/index.d.ts +16 -0
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
- package/dist/shared/{stack.BTStHVTy.d.cts → stack.BGzgFx3R.d.cts} +5 -5
- package/dist/shared/{stack.CtrnoQgH.d.cts → stack.BOZRaVpu.d.cts} +35 -35
- package/dist/shared/{stack.94kN3pEK.d.mts → stack.CjCSDfWy.d.mts} +5 -5
- package/dist/shared/{stack.lPKwd_v0.d.ts → stack.DlBoNqYu.d.ts} +35 -35
- package/dist/shared/{stack.DfrRYOIl.d.ts → stack.Dpg_JjIo.d.ts} +5 -5
- package/dist/shared/{stack.BggTHKlo.d.mts → stack.Dvy7FmaA.d.mts} +35 -35
- package/dist/shared/{stack.5_zzCejP.d.cts → stack.rVF0f509.d.cts} +20 -0
- package/dist/shared/{stack.5_zzCejP.d.mts → stack.rVF0f509.d.mts} +20 -0
- package/dist/shared/{stack.5_zzCejP.d.ts → stack.rVF0f509.d.ts} +20 -0
- package/package.json +11 -7
- package/src/__tests__/package-metadata.test.ts +4 -4
- package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +248 -0
- package/src/plugins/ai-chat/__tests__/hooks.test.tsx +70 -54
- package/src/plugins/ai-chat/client/components/chat-input.tsx +6 -1
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +3 -0
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +9 -2
- package/src/plugins/ai-chat/client/components/chat-message.tsx +13 -5
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +6 -2
- package/src/plugins/ai-chat/client/hooks/chat-hooks.tsx +6 -9
- package/src/plugins/ai-chat/client/overrides.ts +21 -0
- package/src/plugins/blog/__tests__/classnames-overrides.test.tsx +206 -0
- package/src/plugins/blog/client/components/shared/page-wrapper.tsx +3 -2
- package/src/plugins/blog/client/components/shared/post-card.tsx +32 -12
- package/src/plugins/blog/client/components/shared/posts-list.tsx +3 -2
- package/src/plugins/blog/client/overrides.ts +18 -0
- package/src/plugins/cms/__tests__/classnames-overrides.test.tsx +200 -0
- package/src/plugins/cms/client/components/pages/content-list-page.internal.tsx +17 -4
- package/src/plugins/cms/client/components/pages/dashboard-page.internal.tsx +14 -3
- package/src/plugins/cms/client/components/shared/page-wrapper.tsx +3 -2
- package/src/plugins/cms/client/overrides.ts +19 -0
- package/src/plugins/form-builder/__tests__/classnames-overrides.test.tsx +312 -0
- package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +14 -9
- package/src/plugins/form-builder/client/components/pages/form-list-page.internal.tsx +9 -5
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +9 -5
- package/src/plugins/form-builder/client/components/shared/page-wrapper.tsx +3 -2
- package/src/plugins/form-builder/client/components/shared/pagination.tsx +7 -1
- package/src/plugins/form-builder/client/overrides.ts +17 -0
- package/src/plugins/kanban/__tests__/getters.test.ts +14 -1
- package/src/plugins/kanban/api/getters.ts +9 -7
- package/src/plugins/kanban/types.ts +4 -6
- package/src/plugins/media/client/hooks/use-media.tsx +7 -8
- package/dist/shared/{stack.lsbdKRj7.d.mts → stack.B8PG21zX.d.mts} +16 -16
- package/dist/shared/{stack.CKc_Bais.d.cts → stack.Csi-4Mgz.d.cts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.mts → stack.Csi-4Mgz.d.mts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.ts → stack.Csi-4Mgz.d.ts} +7 -7
- package/dist/shared/{stack.BMlTZnZp.d.ts → stack.CyalJ0j6.d.ts} +16 -16
- package/dist/shared/{stack.BV0uNV4f.d.cts → stack.D6Pg-ufX.d.cts} +16 -16
|
@@ -41,6 +41,24 @@ export interface BlogPluginOverrides {
|
|
|
41
41
|
PostCard?: ComponentType<{
|
|
42
42
|
post: SerializedPost;
|
|
43
43
|
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Optional class names merged into the blog UI's structural elements,
|
|
46
|
+
* allowing styling without replacing whole components.
|
|
47
|
+
*/
|
|
48
|
+
classNames?: {
|
|
49
|
+
/** Page container shared by blog pages. */
|
|
50
|
+
container?: string;
|
|
51
|
+
/** Each built-in post card. */
|
|
52
|
+
postCard?: string;
|
|
53
|
+
/** Title inside a post card. */
|
|
54
|
+
postTitle?: string;
|
|
55
|
+
/** Date metadata inside a post card. */
|
|
56
|
+
postMeta?: string;
|
|
57
|
+
/** Optional flex wrapper around post-card tags; omitted when unset. */
|
|
58
|
+
tagsList?: string;
|
|
59
|
+
/** Wrapper around the load-more control. */
|
|
60
|
+
pagination?: string;
|
|
61
|
+
};
|
|
44
62
|
/**
|
|
45
63
|
* Function used to upload a new image file and return its URL.
|
|
46
64
|
* This is separate from `imagePicker`, which selects an existing asset URL.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
// Core primitives MUST be imported from the package entry (not relative src
|
|
6
|
+
// paths) so they share module identity — and React context — with the cms
|
|
7
|
+
// components, which resolve `@btst/stack/*` via package self-reference.
|
|
8
|
+
import { StackProvider } from "@btst/stack/context";
|
|
9
|
+
import { createTestClientStack } from "../../../__tests__/client-stack-test-utils";
|
|
10
|
+
import { cmsClientPlugin } from "../client";
|
|
11
|
+
import { DashboardPage } from "../client/components/pages/dashboard-page.internal";
|
|
12
|
+
import { ContentListPage } from "../client/components/pages/content-list-page.internal";
|
|
13
|
+
import type {
|
|
14
|
+
SerializedContentItemWithType,
|
|
15
|
+
SerializedContentType,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
19
|
+
|
|
20
|
+
// jsdom lacks these APIs used by Radix / cmdk
|
|
21
|
+
(globalThis as any).ResizeObserver ??= class {
|
|
22
|
+
observe() {}
|
|
23
|
+
unobserve() {}
|
|
24
|
+
disconnect() {}
|
|
25
|
+
};
|
|
26
|
+
Element.prototype.scrollIntoView ??= () => {};
|
|
27
|
+
|
|
28
|
+
const hooks = vi.hoisted(() => ({
|
|
29
|
+
useContentTypes: vi.fn(),
|
|
30
|
+
useSuspenseContentTypes: vi.fn(),
|
|
31
|
+
useContent: vi.fn(),
|
|
32
|
+
useSuspenseContent: vi.fn(),
|
|
33
|
+
useDeleteContent: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("../client/hooks", () => hooks);
|
|
37
|
+
|
|
38
|
+
const CLASS_NAMES = {
|
|
39
|
+
container: "bg-red-100",
|
|
40
|
+
contentTypeCard: "bg-blue-100",
|
|
41
|
+
contentTypeTitle: "text-purple-700",
|
|
42
|
+
table: "bg-yellow-50",
|
|
43
|
+
tableRow: "text-green-600",
|
|
44
|
+
pagination: "border-4 border-orange-500",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
const SIMPLE_JSON_SCHEMA = JSON.stringify({
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: { title: { type: "string" } },
|
|
50
|
+
required: ["title"],
|
|
51
|
+
autoFormVersion: 2,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const contentType: SerializedContentType & { itemCount: number } = {
|
|
55
|
+
id: "ct1",
|
|
56
|
+
name: "Post",
|
|
57
|
+
slug: "post",
|
|
58
|
+
description: "",
|
|
59
|
+
jsonSchema: SIMPLE_JSON_SCHEMA,
|
|
60
|
+
autoFormVersion: 2,
|
|
61
|
+
itemCount: 1,
|
|
62
|
+
createdAt: new Date("2024-01-01").toISOString(),
|
|
63
|
+
updatedAt: new Date("2024-01-01").toISOString(),
|
|
64
|
+
} as unknown as SerializedContentType & { itemCount: number };
|
|
65
|
+
|
|
66
|
+
const item: SerializedContentItemWithType = {
|
|
67
|
+
id: "i1",
|
|
68
|
+
slug: "hello-world",
|
|
69
|
+
contentTypeId: "ct1",
|
|
70
|
+
data: JSON.stringify({ title: "Hello" }),
|
|
71
|
+
parsedData: { title: "Hello" },
|
|
72
|
+
contentType: { id: "ct1", name: "Post", slug: "post" },
|
|
73
|
+
createdAt: new Date("2024-01-01").toISOString(),
|
|
74
|
+
updatedAt: new Date("2024-01-01").toISOString(),
|
|
75
|
+
authorId: "user-1",
|
|
76
|
+
} as unknown as SerializedContentItemWithType;
|
|
77
|
+
|
|
78
|
+
let container: HTMLDivElement;
|
|
79
|
+
let root: Root;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
container = document.createElement("div");
|
|
83
|
+
document.body.appendChild(container);
|
|
84
|
+
root = createRoot(container);
|
|
85
|
+
|
|
86
|
+
hooks.useSuspenseContentTypes.mockReturnValue({
|
|
87
|
+
contentTypes: [contentType],
|
|
88
|
+
refetch: vi.fn(),
|
|
89
|
+
});
|
|
90
|
+
hooks.useSuspenseContent.mockReturnValue({
|
|
91
|
+
items: [item],
|
|
92
|
+
total: 1,
|
|
93
|
+
loadMore: vi.fn(),
|
|
94
|
+
hasMore: false,
|
|
95
|
+
isLoadingMore: false,
|
|
96
|
+
refetch: vi.fn(),
|
|
97
|
+
});
|
|
98
|
+
hooks.useContent.mockReturnValue({
|
|
99
|
+
items: [],
|
|
100
|
+
total: 0,
|
|
101
|
+
isLoading: false,
|
|
102
|
+
error: null,
|
|
103
|
+
loadMore: vi.fn(),
|
|
104
|
+
hasMore: false,
|
|
105
|
+
isLoadingMore: false,
|
|
106
|
+
});
|
|
107
|
+
hooks.useDeleteContent.mockReturnValue({
|
|
108
|
+
mutateAsync: vi.fn(),
|
|
109
|
+
isPending: false,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(async () => {
|
|
114
|
+
await act(async () => {
|
|
115
|
+
root.unmount();
|
|
116
|
+
});
|
|
117
|
+
container.remove();
|
|
118
|
+
vi.clearAllMocks();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function createMockRouter() {
|
|
122
|
+
let params = new URLSearchParams();
|
|
123
|
+
return {
|
|
124
|
+
navigate: vi.fn(),
|
|
125
|
+
getSearchParams: () => new URLSearchParams(params.toString()),
|
|
126
|
+
setSearchParams: vi.fn((next: URLSearchParams) => {
|
|
127
|
+
params = new URLSearchParams(next.toString());
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function render(ui: React.ReactElement, withOverrides = true) {
|
|
133
|
+
await act(async () => {
|
|
134
|
+
root.render(
|
|
135
|
+
<StackProvider
|
|
136
|
+
stack={createTestClientStack({ cms: cmsClientPlugin() })}
|
|
137
|
+
router={createMockRouter()}
|
|
138
|
+
overrides={withOverrides ? { cms: { classNames: CLASS_NAMES } } : {}}
|
|
139
|
+
>
|
|
140
|
+
{ui}
|
|
141
|
+
</StackProvider>,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function query(selector: string): HTMLElement {
|
|
147
|
+
const el = container.querySelector<HTMLElement>(selector);
|
|
148
|
+
expect(el, `expected to find ${selector}`).toBeTruthy();
|
|
149
|
+
return el!;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe("cms classNames overrides (issue #36)", () => {
|
|
153
|
+
it("applies container, contentTypeCard and contentTypeTitle on the dashboard", async () => {
|
|
154
|
+
await render(<DashboardPage />);
|
|
155
|
+
|
|
156
|
+
const page = query('[data-testid="cms-dashboard-page"]');
|
|
157
|
+
expect(page.className).toContain(CLASS_NAMES.container);
|
|
158
|
+
// The override must merge with the layout's own classes, not replace them.
|
|
159
|
+
expect(page.className).toContain("mx-auto");
|
|
160
|
+
|
|
161
|
+
const card = query('[data-slot="card"]');
|
|
162
|
+
expect(card.className).toContain(CLASS_NAMES.contentTypeCard);
|
|
163
|
+
expect(card.className).toContain("cursor-pointer");
|
|
164
|
+
|
|
165
|
+
const title = query('[data-slot="card-title"]');
|
|
166
|
+
expect(title.className).toContain(CLASS_NAMES.contentTypeTitle);
|
|
167
|
+
expect(title.className).toContain("font-medium");
|
|
168
|
+
expect(title.textContent).toBe(contentType.name);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("applies table, tableRow and pagination on the content list", async () => {
|
|
172
|
+
await render(<ContentListPage typeSlug="post" />);
|
|
173
|
+
|
|
174
|
+
const table = query('[data-testid="cms-content-table"]');
|
|
175
|
+
expect(table.className).toContain(CLASS_NAMES.table);
|
|
176
|
+
expect(table.className).toContain("rounded-lg");
|
|
177
|
+
|
|
178
|
+
const row = table.querySelector<HTMLElement>("tbody tr")!;
|
|
179
|
+
expect(row, "expected a body row").toBeTruthy();
|
|
180
|
+
expect(row.className).toContain(CLASS_NAMES.tableRow);
|
|
181
|
+
expect(row.textContent).toContain(item.slug);
|
|
182
|
+
|
|
183
|
+
const pagination = query('[data-testid="cms-content-pagination"]');
|
|
184
|
+
expect(pagination.className).toContain("border-4");
|
|
185
|
+
expect(pagination.className).toContain("border-orange-500");
|
|
186
|
+
// Non-conflicting base classes survive the merge. (`border-t` is
|
|
187
|
+
// intentionally dropped by tailwind-merge since `border-4` wins the
|
|
188
|
+
// border-width group — that is the override taking precedence.)
|
|
189
|
+
expect(pagination.className).toContain("justify-between");
|
|
190
|
+
expect(pagination.className).toContain("px-4");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("leaves base classes untouched when no classNames override is supplied", async () => {
|
|
194
|
+
await render(<DashboardPage />, false);
|
|
195
|
+
|
|
196
|
+
const card = query('[data-slot="card"]');
|
|
197
|
+
expect(card.className).toContain("cursor-pointer");
|
|
198
|
+
expect(card.className).not.toContain(CLASS_NAMES.contentTypeCard);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -25,6 +25,7 @@ import { cmsPermissions } from "../../../permissions";
|
|
|
25
25
|
import { useListState, type ListStateSchema } from "@btst/stack/client/hooks";
|
|
26
26
|
import { orderCMSContentTypes, type CMSPluginOverrides } from "../../overrides";
|
|
27
27
|
import { CMS_PLUGIN_ID } from "../../constants";
|
|
28
|
+
import { cn } from "../../../utils";
|
|
28
29
|
import type { SerializedContentItemWithType } from "../../../types";
|
|
29
30
|
import {
|
|
30
31
|
useContent,
|
|
@@ -52,7 +53,7 @@ export function ContentListPage({ typeSlug }: ContentListPageProps) {
|
|
|
52
53
|
const t = useTranslate();
|
|
53
54
|
const notify = useNotify();
|
|
54
55
|
const overrides = usePluginOverrides<CMSPluginOverrides>(CMS_PLUGIN_ID);
|
|
55
|
-
const { localization } = overrides;
|
|
56
|
+
const { localization, classNames } = overrides;
|
|
56
57
|
const { router, plugins, basePath: stackBasePath } = useStack();
|
|
57
58
|
const navigate = router?.navigate;
|
|
58
59
|
const Link = router?.Link;
|
|
@@ -287,6 +288,7 @@ export function ContentListPage({ typeSlug }: ContentListPageProps) {
|
|
|
287
288
|
hasMore={hasMore}
|
|
288
289
|
isLoadingMore={isLoadingMore}
|
|
289
290
|
localization={localization}
|
|
291
|
+
classNames={classNames}
|
|
290
292
|
t={t}
|
|
291
293
|
/>
|
|
292
294
|
)}
|
|
@@ -309,6 +311,7 @@ function ContentTable({
|
|
|
309
311
|
hasMore,
|
|
310
312
|
isLoadingMore,
|
|
311
313
|
localization,
|
|
314
|
+
classNames,
|
|
312
315
|
t,
|
|
313
316
|
}: {
|
|
314
317
|
items: SerializedContentItemWithType[];
|
|
@@ -324,10 +327,14 @@ function ContentTable({
|
|
|
324
327
|
hasMore: boolean;
|
|
325
328
|
isLoadingMore: boolean;
|
|
326
329
|
localization: CMSPluginOverrides["localization"];
|
|
330
|
+
classNames: CMSPluginOverrides["classNames"];
|
|
327
331
|
t: TranslateFn;
|
|
328
332
|
}) {
|
|
329
333
|
return (
|
|
330
|
-
<div
|
|
334
|
+
<div
|
|
335
|
+
className={cn("border rounded-lg", classNames?.table)}
|
|
336
|
+
data-testid="cms-content-table"
|
|
337
|
+
>
|
|
331
338
|
<Table>
|
|
332
339
|
<TableHeader>
|
|
333
340
|
<TableRow>
|
|
@@ -351,7 +358,7 @@ function ContentTable({
|
|
|
351
358
|
</TableHeader>
|
|
352
359
|
<TableBody>
|
|
353
360
|
{items.map((item) => (
|
|
354
|
-
<TableRow key={item.id}>
|
|
361
|
+
<TableRow key={item.id} className={classNames?.tableRow}>
|
|
355
362
|
<TableCell className="font-medium">
|
|
356
363
|
<LinkComponent
|
|
357
364
|
href={joinBasePath(basePath, `/cms/${typeSlug}/${item.id}`)}
|
|
@@ -406,7 +413,13 @@ function ContentTable({
|
|
|
406
413
|
</TableBody>
|
|
407
414
|
</Table>
|
|
408
415
|
{/* Load More and pagination info */}
|
|
409
|
-
<div
|
|
416
|
+
<div
|
|
417
|
+
className={cn(
|
|
418
|
+
"flex items-center justify-between px-4 py-3 border-t",
|
|
419
|
+
classNames?.pagination,
|
|
420
|
+
)}
|
|
421
|
+
data-testid="cms-content-pagination"
|
|
422
|
+
>
|
|
410
423
|
<p className="text-sm text-muted-foreground">
|
|
411
424
|
{(
|
|
412
425
|
localization?.CMS_LIST_PAGINATION_SHOWING ??
|
|
@@ -19,11 +19,12 @@ import { useSuspenseContentTypes } from "../../hooks";
|
|
|
19
19
|
import { EmptyState } from "../shared/empty-state";
|
|
20
20
|
import { PageWrapper } from "../shared/page-wrapper";
|
|
21
21
|
import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
|
|
22
|
+
import { cn } from "../../../utils";
|
|
22
23
|
|
|
23
24
|
export function DashboardPage() {
|
|
24
25
|
const t = useTranslate();
|
|
25
26
|
const overrides = usePluginOverrides<CMSPluginOverrides>(CMS_PLUGIN_ID);
|
|
26
|
-
const { localization } = overrides;
|
|
27
|
+
const { localization, classNames } = overrides;
|
|
27
28
|
const { router, plugins, basePath: stackBasePath } = useStack();
|
|
28
29
|
const navigate = router?.navigate;
|
|
29
30
|
const basePath = plugins?.[CMS_PLUGIN_ID]?.site.basePath ?? stackBasePath;
|
|
@@ -104,13 +105,23 @@ export function DashboardPage() {
|
|
|
104
105
|
{contentTypes.map((ct) => (
|
|
105
106
|
<Card
|
|
106
107
|
key={ct.id}
|
|
107
|
-
className=
|
|
108
|
+
className={cn(
|
|
109
|
+
"hover:border-primary/50 transition-colors cursor-pointer",
|
|
110
|
+
classNames?.contentTypeCard,
|
|
111
|
+
)}
|
|
108
112
|
onClick={() =>
|
|
109
113
|
void navigate?.(joinBasePath(basePath, `/cms/${ct.slug}`))
|
|
110
114
|
}
|
|
111
115
|
>
|
|
112
116
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
113
|
-
<CardTitle
|
|
117
|
+
<CardTitle
|
|
118
|
+
className={cn(
|
|
119
|
+
"text-lg font-medium",
|
|
120
|
+
classNames?.contentTypeTitle,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{ct.name}
|
|
124
|
+
</CardTitle>
|
|
114
125
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
115
126
|
</CardHeader>
|
|
116
127
|
<CardContent>
|
|
@@ -4,6 +4,7 @@ import { usePluginOverrides } from "@btst/stack/context";
|
|
|
4
4
|
import { PageWrapper as SharedPageWrapper } from "@workspace/ui/components/page-wrapper";
|
|
5
5
|
import type { CMSPluginOverrides } from "../../overrides";
|
|
6
6
|
import { CMS_PLUGIN_ID } from "../../constants";
|
|
7
|
+
import { cn } from "../../../utils";
|
|
7
8
|
|
|
8
9
|
export function PageWrapper({
|
|
9
10
|
children,
|
|
@@ -14,7 +15,7 @@ export function PageWrapper({
|
|
|
14
15
|
className?: string;
|
|
15
16
|
testId?: string;
|
|
16
17
|
}) {
|
|
17
|
-
const { showAttribution } = usePluginOverrides<
|
|
18
|
+
const { showAttribution, classNames } = usePluginOverrides<
|
|
18
19
|
CMSPluginOverrides,
|
|
19
20
|
Partial<CMSPluginOverrides>
|
|
20
21
|
>(CMS_PLUGIN_ID, {
|
|
@@ -23,7 +24,7 @@ export function PageWrapper({
|
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
26
|
<SharedPageWrapper
|
|
26
|
-
className={className}
|
|
27
|
+
className={cn(className, classNames?.container)}
|
|
27
28
|
testId={testId}
|
|
28
29
|
showAttribution={showAttribution}
|
|
29
30
|
>
|
|
@@ -129,6 +129,25 @@ export interface CMSPluginOverrides {
|
|
|
129
129
|
*/
|
|
130
130
|
imagePicker?: ComponentType<{ onSelect: (url: string) => void }>;
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Optional class names merged onto specific UI elements.
|
|
134
|
+
* Use these for targeted styling without replacing whole components.
|
|
135
|
+
*/
|
|
136
|
+
classNames?: {
|
|
137
|
+
/** Page container shared by every CMS page */
|
|
138
|
+
container?: string;
|
|
139
|
+
/** Each content-type card on the dashboard */
|
|
140
|
+
contentTypeCard?: string;
|
|
141
|
+
/** Title inside a content-type card */
|
|
142
|
+
contentTypeTitle?: string;
|
|
143
|
+
/** Wrapper around the content entries table */
|
|
144
|
+
table?: string;
|
|
145
|
+
/** Each row in the content entries table */
|
|
146
|
+
tableRow?: string;
|
|
147
|
+
/** Pagination footer under the content entries table */
|
|
148
|
+
pagination?: string;
|
|
149
|
+
};
|
|
150
|
+
|
|
132
151
|
/**
|
|
133
152
|
* Custom field components for AutoForm fields.
|
|
134
153
|
*
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
// Core primitives MUST be imported from the package entry (not relative src
|
|
7
|
+
// paths) so they share module identity — and React context — with the
|
|
8
|
+
// form-builder components, which resolve `@btst/stack/*` via package
|
|
9
|
+
// self-reference.
|
|
10
|
+
import { StackProvider } from "@btst/stack/context";
|
|
11
|
+
import { defineAuthorization } from "@btst/stack/authorization";
|
|
12
|
+
import { createClientAuth } from "@btst/stack/authorization/client";
|
|
13
|
+
import { FormRenderer } from "../client/components/forms/form-renderer";
|
|
14
|
+
import { FormListPage } from "../client/components/pages/form-list-page.internal";
|
|
15
|
+
import { SubmissionsPage } from "../client/components/pages/submissions-page.internal";
|
|
16
|
+
import { formBuilderClientPlugin } from "../client/plugin";
|
|
17
|
+
import { formBuilderPermissions } from "../permissions";
|
|
18
|
+
import type { SerializedForm } from "../types";
|
|
19
|
+
import { createTestClientStack } from "../../../__tests__/client-stack-test-utils";
|
|
20
|
+
|
|
21
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
|
+
|
|
23
|
+
// jsdom lacks these APIs used by Radix
|
|
24
|
+
(globalThis as any).ResizeObserver ??= class {
|
|
25
|
+
observe() {}
|
|
26
|
+
unobserve() {}
|
|
27
|
+
disconnect() {}
|
|
28
|
+
};
|
|
29
|
+
Element.prototype.scrollIntoView ??= () => {};
|
|
30
|
+
|
|
31
|
+
const listHooks = vi.hoisted(() => ({
|
|
32
|
+
useForms: vi.fn(),
|
|
33
|
+
useSuspenseForms: vi.fn(),
|
|
34
|
+
useDeleteForm: vi.fn(),
|
|
35
|
+
useSuspenseFormById: vi.fn(),
|
|
36
|
+
useSuspenseFormForUpdate: vi.fn(),
|
|
37
|
+
useSuspenseSubmissions: vi.fn(),
|
|
38
|
+
useSubmission: vi.fn(),
|
|
39
|
+
useDeleteSubmission: vi.fn(),
|
|
40
|
+
useFormBuilderForm: vi.fn(),
|
|
41
|
+
}));
|
|
42
|
+
const rendererHooks = vi.hoisted(() => ({
|
|
43
|
+
useFormBySlug: vi.fn(),
|
|
44
|
+
useSubmitForm: vi.fn(),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("../client/hooks", () => listHooks);
|
|
48
|
+
vi.mock("../client/hooks/form-builder-hooks", () => rendererHooks);
|
|
49
|
+
|
|
50
|
+
// The auto-form internals are unrelated to class wiring — stub them.
|
|
51
|
+
vi.mock("@workspace/ui/components/auto-form/stepped-auto-form", () => ({
|
|
52
|
+
SteppedAutoForm: ({
|
|
53
|
+
onSubmit,
|
|
54
|
+
}: {
|
|
55
|
+
onSubmit: (values: Record<string, unknown>) => void;
|
|
56
|
+
}) => (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
data-testid="stepped-auto-form"
|
|
60
|
+
onClick={() => onSubmit({})}
|
|
61
|
+
>
|
|
62
|
+
Submit
|
|
63
|
+
</button>
|
|
64
|
+
),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const CLASS_NAMES = {
|
|
68
|
+
container: "bg-red-100",
|
|
69
|
+
table: "bg-yellow-50",
|
|
70
|
+
tableRow: "text-green-600",
|
|
71
|
+
pagination: "border-4 border-orange-500",
|
|
72
|
+
form: "bg-blue-100",
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
const form: SerializedForm = {
|
|
76
|
+
id: "f1",
|
|
77
|
+
name: "Contact Form",
|
|
78
|
+
slug: "contact-form",
|
|
79
|
+
description: null,
|
|
80
|
+
schema: JSON.stringify({ type: "object", properties: {} }),
|
|
81
|
+
successMessage: null,
|
|
82
|
+
redirectUrl: null,
|
|
83
|
+
status: "active",
|
|
84
|
+
createdBy: null,
|
|
85
|
+
createdAt: new Date("2024-01-01").toISOString(),
|
|
86
|
+
updatedAt: new Date("2024-01-01").toISOString(),
|
|
87
|
+
} as unknown as SerializedForm;
|
|
88
|
+
|
|
89
|
+
const authorization = defineAuthorization({
|
|
90
|
+
identity: z.object({ id: z.string(), role: z.string() }),
|
|
91
|
+
permissions: [formBuilderPermissions] as const,
|
|
92
|
+
rules: ({ forms }) => [
|
|
93
|
+
forms.form.render.allow(),
|
|
94
|
+
forms.submission.create.allow(),
|
|
95
|
+
forms.submission.read.allow(),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
const auth = createClientAuth({ authorization, getIdentity: () => null });
|
|
99
|
+
|
|
100
|
+
let container: HTMLDivElement;
|
|
101
|
+
let root: Root;
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
container = document.createElement("div");
|
|
105
|
+
document.body.appendChild(container);
|
|
106
|
+
root = createRoot(container);
|
|
107
|
+
|
|
108
|
+
listHooks.useSuspenseForms.mockReturnValue({
|
|
109
|
+
forms: [form],
|
|
110
|
+
total: 1,
|
|
111
|
+
loadMore: vi.fn(),
|
|
112
|
+
hasMore: false,
|
|
113
|
+
isLoadingMore: false,
|
|
114
|
+
refetch: vi.fn(),
|
|
115
|
+
});
|
|
116
|
+
listHooks.useForms.mockReturnValue({
|
|
117
|
+
forms: [],
|
|
118
|
+
total: 0,
|
|
119
|
+
isLoading: false,
|
|
120
|
+
error: null,
|
|
121
|
+
loadMore: vi.fn(),
|
|
122
|
+
hasMore: false,
|
|
123
|
+
isLoadingMore: false,
|
|
124
|
+
refetch: vi.fn(),
|
|
125
|
+
});
|
|
126
|
+
listHooks.useDeleteForm.mockReturnValue({
|
|
127
|
+
mutateAsync: vi.fn(),
|
|
128
|
+
isPending: false,
|
|
129
|
+
});
|
|
130
|
+
rendererHooks.useFormBySlug.mockReturnValue({
|
|
131
|
+
form,
|
|
132
|
+
isLoading: false,
|
|
133
|
+
error: null,
|
|
134
|
+
});
|
|
135
|
+
rendererHooks.useSubmitForm.mockReturnValue({
|
|
136
|
+
mutateAsync: vi.fn(),
|
|
137
|
+
isPending: false,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(async () => {
|
|
142
|
+
await act(async () => {
|
|
143
|
+
root.unmount();
|
|
144
|
+
});
|
|
145
|
+
container.remove();
|
|
146
|
+
vi.clearAllMocks();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function createMockRouter() {
|
|
150
|
+
let params = new URLSearchParams();
|
|
151
|
+
return {
|
|
152
|
+
navigate: vi.fn(),
|
|
153
|
+
getSearchParams: () => new URLSearchParams(params.toString()),
|
|
154
|
+
setSearchParams: vi.fn((next: URLSearchParams) => {
|
|
155
|
+
params = new URLSearchParams(next.toString());
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function render(ui: React.ReactElement, withOverrides = true) {
|
|
161
|
+
await act(async () => {
|
|
162
|
+
root.render(
|
|
163
|
+
<StackProvider
|
|
164
|
+
stack={createTestClientStack({
|
|
165
|
+
formBuilder: formBuilderClientPlugin(),
|
|
166
|
+
})}
|
|
167
|
+
router={createMockRouter()}
|
|
168
|
+
auth={auth}
|
|
169
|
+
initialIdentity={null}
|
|
170
|
+
overrides={
|
|
171
|
+
withOverrides ? { formBuilder: { classNames: CLASS_NAMES } } : {}
|
|
172
|
+
}
|
|
173
|
+
>
|
|
174
|
+
{ui}
|
|
175
|
+
</StackProvider>,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function query(selector: string): HTMLElement {
|
|
181
|
+
const el = container.querySelector<HTMLElement>(selector);
|
|
182
|
+
expect(el, `expected to find ${selector}`).toBeTruthy();
|
|
183
|
+
return el!;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe("form-builder classNames overrides (issue #36)", () => {
|
|
187
|
+
it("applies container, table, tableRow and pagination on the forms list", async () => {
|
|
188
|
+
await render(<FormListPage />);
|
|
189
|
+
|
|
190
|
+
const page = query('[data-testid="form-list-page"]');
|
|
191
|
+
expect(page.className).toContain(CLASS_NAMES.container);
|
|
192
|
+
// The override must merge with the layout's own classes, not replace them.
|
|
193
|
+
expect(page.className).toContain("mx-auto");
|
|
194
|
+
|
|
195
|
+
const table = query('[data-testid="form-builder-list-table"]');
|
|
196
|
+
expect(table.className).toContain(CLASS_NAMES.table);
|
|
197
|
+
expect(table.className).toContain("rounded-lg");
|
|
198
|
+
|
|
199
|
+
const row = table.querySelector<HTMLElement>("tbody tr")!;
|
|
200
|
+
expect(row, "expected a body row").toBeTruthy();
|
|
201
|
+
expect(row.className).toContain(CLASS_NAMES.tableRow);
|
|
202
|
+
expect(row.textContent).toContain(form.name);
|
|
203
|
+
|
|
204
|
+
const pagination = query('[data-testid="form-builder-pagination"]');
|
|
205
|
+
expect(pagination.className).toContain("border-4");
|
|
206
|
+
expect(pagination.className).toContain("border-orange-500");
|
|
207
|
+
expect(pagination.className).toContain("justify-between");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("applies table, tableRow and pagination on the submissions page", async () => {
|
|
211
|
+
listHooks.useSuspenseSubmissions.mockReturnValue({
|
|
212
|
+
form,
|
|
213
|
+
submissions: [
|
|
214
|
+
{
|
|
215
|
+
id: "sub-11111111",
|
|
216
|
+
formId: form.id,
|
|
217
|
+
submittedAt: new Date("2024-01-02").toISOString(),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
total: 1,
|
|
221
|
+
loadMore: vi.fn(),
|
|
222
|
+
hasMore: false,
|
|
223
|
+
isLoadingMore: false,
|
|
224
|
+
refetch: vi.fn(),
|
|
225
|
+
});
|
|
226
|
+
listHooks.useSubmission.mockReturnValue({
|
|
227
|
+
submission: null,
|
|
228
|
+
isLoading: false,
|
|
229
|
+
error: null,
|
|
230
|
+
refetch: vi.fn(),
|
|
231
|
+
});
|
|
232
|
+
listHooks.useDeleteSubmission.mockReturnValue({
|
|
233
|
+
mutateAsync: vi.fn(),
|
|
234
|
+
isPending: false,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await render(<SubmissionsPage formId={form.id} />);
|
|
238
|
+
|
|
239
|
+
const table = query('[data-testid="form-builder-submissions-table"]');
|
|
240
|
+
expect(table.className).toContain(CLASS_NAMES.table);
|
|
241
|
+
expect(table.className).toContain("rounded-lg");
|
|
242
|
+
|
|
243
|
+
const row = table.querySelector<HTMLElement>("tbody tr")!;
|
|
244
|
+
expect(row, "expected a body row").toBeTruthy();
|
|
245
|
+
expect(row.className).toContain(CLASS_NAMES.tableRow);
|
|
246
|
+
|
|
247
|
+
const pagination = query('[data-testid="form-builder-pagination"]');
|
|
248
|
+
expect(pagination.className).toContain("border-orange-500");
|
|
249
|
+
expect(pagination.className).toContain("justify-between");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("applies classNames.form to the rendered public form", async () => {
|
|
253
|
+
await render(<FormRenderer slug={form.slug} />);
|
|
254
|
+
|
|
255
|
+
const root = query('[data-testid="form-renderer"]');
|
|
256
|
+
expect(root.className).toContain(CLASS_NAMES.form);
|
|
257
|
+
// The real auto-form is stubbed, but it must still be mounted inside the slot.
|
|
258
|
+
expect(
|
|
259
|
+
root.querySelector('[data-testid="stepped-auto-form"]'),
|
|
260
|
+
).toBeTruthy();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it.each([
|
|
264
|
+
["loading", { form: null, isLoading: true, error: null }],
|
|
265
|
+
[
|
|
266
|
+
"error",
|
|
267
|
+
{ form: null, isLoading: false, error: new Error("Unavailable") },
|
|
268
|
+
],
|
|
269
|
+
["not found", { form: null, isLoading: false, error: null }],
|
|
270
|
+
[
|
|
271
|
+
"inactive",
|
|
272
|
+
{ form: { ...form, status: "inactive" }, isLoading: false, error: null },
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
"invalid schema",
|
|
276
|
+
{ form: { ...form, schema: "invalid" }, isLoading: false, error: null },
|
|
277
|
+
],
|
|
278
|
+
])("preserves form classes in the %s state", async (_state, result) => {
|
|
279
|
+
rendererHooks.useFormBySlug.mockReturnValue(result);
|
|
280
|
+
await render(<FormRenderer slug={form.slug} className="p-6" />);
|
|
281
|
+
const wrapper = container.firstElementChild!;
|
|
282
|
+
expect(wrapper.className).toContain(CLASS_NAMES.form);
|
|
283
|
+
expect(wrapper.className).toContain("p-6");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("preserves form classes after successful submission", async () => {
|
|
287
|
+
const submit = vi
|
|
288
|
+
.fn()
|
|
289
|
+
.mockResolvedValue({ form: { successMessage: "Thank you!" } });
|
|
290
|
+
rendererHooks.useSubmitForm.mockReturnValue({
|
|
291
|
+
mutateAsync: submit,
|
|
292
|
+
isPending: false,
|
|
293
|
+
});
|
|
294
|
+
await render(<FormRenderer slug={form.slug} className="p-6" />);
|
|
295
|
+
await act(async () => {
|
|
296
|
+
query('[data-testid="stepped-auto-form"]').click();
|
|
297
|
+
});
|
|
298
|
+
expect(submit).toHaveBeenCalled();
|
|
299
|
+
expect(container.textContent).toContain("Thank you!");
|
|
300
|
+
const wrapper = container.firstElementChild!;
|
|
301
|
+
expect(wrapper.className).toContain(CLASS_NAMES.form);
|
|
302
|
+
expect(wrapper.className).toContain("p-6");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("leaves base classes untouched when no classNames override is supplied", async () => {
|
|
306
|
+
await render(<FormListPage />, false);
|
|
307
|
+
|
|
308
|
+
const table = query('[data-testid="form-builder-list-table"]');
|
|
309
|
+
expect(table.className).toContain("rounded-lg");
|
|
310
|
+
expect(table.className).not.toContain(CLASS_NAMES.table);
|
|
311
|
+
});
|
|
312
|
+
});
|