@carlonicora/nextjs-jsonapi 1.16.0 → 1.17.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/dist/ApiData-DPKNfY-9.d.mts +10 -0
- package/dist/ApiData-DPKNfY-9.d.ts +10 -0
- package/dist/ApiRequestDataTypeInterface-DIEOFn9s.d.mts +40 -0
- package/dist/ApiRequestDataTypeInterface-DIEOFn9s.d.ts +40 -0
- package/dist/{ApiResponseInterface-BvWIeLkq.d.ts → ApiResponseInterface-BKyod24U.d.ts} +2 -11
- package/dist/{ApiResponseInterface-CAbw0sv7.d.mts → ApiResponseInterface-Dqvu09tz.d.mts} +2 -11
- package/dist/{BlockNoteEditor-MBFDWP7X.js → BlockNoteEditor-34T5CY27.js} +17 -16
- package/dist/BlockNoteEditor-34T5CY27.js.map +1 -0
- package/dist/{BlockNoteEditor-HFX7Z5BQ.mjs → BlockNoteEditor-4Z6TZBJE.mjs} +7 -6
- package/dist/{BlockNoteEditor-HFX7Z5BQ.mjs.map → BlockNoteEditor-4Z6TZBJE.mjs.map} +1 -1
- package/dist/JsonApiContext-Bsm_Q2oe.d.mts +41 -0
- package/dist/JsonApiContext-Bsm_Q2oe.d.ts +41 -0
- package/dist/JsonApiRequest-54ZBO7WQ.js +24 -0
- package/dist/{JsonApiRequest-45CLE65I.js.map → JsonApiRequest-54ZBO7WQ.js.map} +1 -1
- package/dist/{JsonApiRequest-6IPS3DZJ.mjs → JsonApiRequest-XWQWTFEQ.mjs} +2 -2
- package/dist/chunk-3EPNHTMH.js +26 -0
- package/dist/chunk-3EPNHTMH.js.map +1 -0
- package/dist/{chunk-BCKYJQ3K.mjs → chunk-3VM3WAOV.mjs} +1 -1
- package/dist/{chunk-R5QSSISB.js → chunk-7DTKRMYW.js} +21 -14
- package/dist/chunk-7DTKRMYW.js.map +1 -0
- package/dist/{chunk-ONB2DAIV.js → chunk-D7H7SRWB.js} +455 -470
- package/dist/chunk-D7H7SRWB.js.map +1 -0
- package/dist/{chunk-BCQSE3EU.mjs → chunk-KUFWHMMY.mjs} +8 -8
- package/dist/{chunk-POKIJ56Q.mjs → chunk-KX7YG6LY.mjs} +22 -15
- package/dist/chunk-KX7YG6LY.mjs.map +1 -0
- package/dist/{chunk-GPGJNTHP.js → chunk-LI6CPNJI.js} +1 -1
- package/dist/{chunk-GPGJNTHP.js.map → chunk-LI6CPNJI.js.map} +1 -1
- package/dist/{chunk-5RAUCUAA.mjs → chunk-SXPXC2TY.mjs} +18 -33
- package/dist/chunk-SXPXC2TY.mjs.map +1 -0
- package/dist/{chunk-2AZLCF6D.js → chunk-UYY34W7R.js} +28 -28
- package/dist/{chunk-2AZLCF6D.js.map → chunk-UYY34W7R.js.map} +1 -1
- package/dist/chunk-VOXD3ZLY.mjs +26 -0
- package/dist/chunk-VOXD3ZLY.mjs.map +1 -0
- package/dist/client/index.d.mts +11 -45
- package/dist/client/index.d.ts +11 -45
- package/dist/client/index.js +9 -7
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +11 -9
- package/dist/components/index.d.mts +4 -3
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.js +7 -6
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +6 -5
- package/dist/{config-DEaUbBqR.d.ts → config--nwiW74Z.d.ts} +1 -1
- package/dist/{config-CWsTwnsK.d.mts → config-BKSQmUWU.d.mts} +1 -1
- package/dist/{content.interface-D_4b4RQt.d.ts → content.interface-4VICFRA0.d.ts} +2 -1
- package/dist/{content.interface-Dk4UZcJM.d.mts → content.interface-CFc97-Cj.d.mts} +2 -1
- package/dist/contexts/index.d.mts +3 -2
- package/dist/contexts/index.d.ts +3 -2
- package/dist/contexts/index.js +7 -6
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +6 -5
- package/dist/core/index.d.mts +11 -8
- package/dist/core/index.d.ts +11 -8
- package/dist/core/index.js +4 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +3 -3
- package/dist/index.d.mts +15 -11
- package/dist/index.d.ts +15 -11
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/{notification.interface-BllkURRm.d.ts → notification.interface-BGaPiCUM.d.mts} +2 -40
- package/dist/{notification.interface-BllkURRm.d.mts → notification.interface-CqwaOIgM.d.ts} +2 -40
- package/dist/{s3.service-BEfGqho0.d.ts → s3.service-BYs88XEE.d.ts} +3 -2
- package/dist/{s3.service-DIQRYe93.d.mts → s3.service-C0BjOdvn.d.mts} +3 -2
- package/dist/server/index.d.mts +6 -4
- package/dist/server/index.d.ts +6 -4
- package/dist/server/index.js +13 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +3 -3
- package/dist/{stripe-subscription.interface-C63L6hVg.d.mts → stripe-subscription.interface-B-TM40Io.d.ts} +1 -1
- package/dist/{stripe-subscription.interface-CUvNDvw5.d.ts → stripe-subscription.interface-DDxnpj0F.d.mts} +1 -1
- package/dist/testing/index.d.mts +338 -0
- package/dist/testing/index.d.ts +338 -0
- package/dist/testing/index.js +323 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +323 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/{useSocket-BpenBR2z.d.mts → useSocket-BNj9PrRw.d.mts} +1 -1
- package/dist/{useSocket-D-QYA0Sr.d.ts → useSocket-Dwt8cz1x.d.ts} +1 -1
- package/package.json +21 -3
- package/src/client/hooks/__tests__/useJsonApiGet.test.tsx +229 -0
- package/src/client/hooks/__tests__/useJsonApiMutation.test.tsx +348 -0
- package/src/client/hooks/__tests__/useRehydration.test.ts +188 -0
- package/src/components/forms/__tests__/FormCheckbox.test.tsx +238 -0
- package/src/components/forms/__tests__/FormDate.test.tsx +212 -0
- package/src/components/forms/__tests__/FormInput.test.tsx +292 -0
- package/src/components/forms/__tests__/FormSelect.test.tsx +173 -0
- package/src/components/tables/__tests__/ContentListTable.test.tsx +411 -0
- package/src/core/endpoint/__tests__/EndpointCreator.test.ts +168 -0
- package/src/core/factories/__tests__/JsonApiDataFactory.test.ts +109 -0
- package/src/core/factories/__tests__/RehydrationFactory.test.ts +151 -0
- package/src/core/registry/__tests__/DataClassRegistry.test.ts +136 -0
- package/src/core/registry/__tests__/ModuleRegistrar.test.ts +159 -0
- package/src/features/auth/components/details/LandingComponent.tsx +14 -12
- package/src/hooks/__tests__/useDataListRetriever.test.ts +321 -0
- package/src/hooks/__tests__/useDebounce.test.ts +170 -0
- package/src/index.ts +4 -1
- package/src/login/config.ts +27 -0
- package/src/login/index.ts +2 -0
- package/src/testing/factories/createMockApiData.ts +143 -0
- package/src/testing/factories/createMockModule.ts +32 -0
- package/src/testing/factories/createMockResponse.ts +93 -0
- package/src/testing/factories/createMockService.ts +79 -0
- package/src/testing/index.ts +70 -0
- package/src/testing/matchers/jsonApiMatchers.ts +174 -0
- package/src/testing/providers/MockJsonApiProvider.tsx +58 -0
- package/src/testing/utils/renderWithProviders.tsx +76 -0
- package/src/utils/__tests__/date-formatter.test.ts +161 -0
- package/src/utils/__tests__/exists.test.ts +100 -0
- package/src/utils/cn.test.ts +44 -0
- package/dist/BlockNoteEditor-MBFDWP7X.js.map +0 -1
- package/dist/JsonApiRequest-45CLE65I.js +0 -24
- package/dist/chunk-5RAUCUAA.mjs.map +0 -1
- package/dist/chunk-ONB2DAIV.js.map +0 -1
- package/dist/chunk-POKIJ56Q.mjs.map +0 -1
- package/dist/chunk-R5QSSISB.js.map +0 -1
- package/src/discord/config.ts +0 -15
- package/src/discord/index.ts +0 -1
- /package/dist/{JsonApiRequest-6IPS3DZJ.mjs.map → JsonApiRequest-XWQWTFEQ.mjs.map} +0 -0
- /package/dist/{chunk-BCKYJQ3K.mjs.map → chunk-3VM3WAOV.mjs.map} +0 -0
- /package/dist/{chunk-BCQSE3EU.mjs.map → chunk-KUFWHMMY.mjs.map} +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { ContentListTable } from "../ContentListTable";
|
|
5
|
+
import { DataListRetriever } from "../../../hooks";
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
// Mock useTableGenerator hook
|
|
9
|
+
vi.mock("../../../hooks", async () => {
|
|
10
|
+
const actual = await vi.importActual("../../../hooks");
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
useTableGenerator: vi.fn((_, params) => ({
|
|
14
|
+
data: params.data.map((item: any, index: number) => ({
|
|
15
|
+
...item,
|
|
16
|
+
_rowIndex: index,
|
|
17
|
+
})),
|
|
18
|
+
columns: [
|
|
19
|
+
{
|
|
20
|
+
id: "id",
|
|
21
|
+
header: "ID",
|
|
22
|
+
accessorKey: "id",
|
|
23
|
+
cell: ({ row }: any) => row.original.id,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "title",
|
|
27
|
+
header: "Title",
|
|
28
|
+
accessorKey: "title",
|
|
29
|
+
cell: ({ row }: any) => row.original.title,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Mock ContentTableSearch
|
|
37
|
+
vi.mock("../ContentTableSearch", () => ({
|
|
38
|
+
ContentTableSearch: () => <div data-testid="content-table-search">Search</div>,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Create a mock DataListRetriever
|
|
42
|
+
function createMockDataRetriever(overrides: Partial<DataListRetriever<any>> = {}): DataListRetriever<any> {
|
|
43
|
+
return {
|
|
44
|
+
ready: true,
|
|
45
|
+
setReady: vi.fn(),
|
|
46
|
+
isLoaded: true,
|
|
47
|
+
data: [],
|
|
48
|
+
search: vi.fn(),
|
|
49
|
+
refresh: vi.fn(),
|
|
50
|
+
addAdditionalParameter: vi.fn(),
|
|
51
|
+
removeAdditionalParameter: vi.fn(),
|
|
52
|
+
setRefreshedElement: vi.fn(),
|
|
53
|
+
removeElement: vi.fn(),
|
|
54
|
+
isSearch: false,
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mockModule = {
|
|
60
|
+
name: "articles",
|
|
61
|
+
model: class MockArticle {},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe("ContentListTable", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("rendering", () => {
|
|
70
|
+
it("should render table", () => {
|
|
71
|
+
const dataRetriever = createMockDataRetriever({
|
|
72
|
+
data: [
|
|
73
|
+
{ id: "1", title: "Article 1" },
|
|
74
|
+
{ id: "2", title: "Article 2" },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
render(
|
|
79
|
+
<ContentListTable
|
|
80
|
+
data={dataRetriever}
|
|
81
|
+
tableGeneratorType={mockModule as any}
|
|
82
|
+
fields={["id", "title"]}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(screen.getByRole("table")).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should render title when provided", () => {
|
|
90
|
+
const dataRetriever = createMockDataRetriever({
|
|
91
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
render(
|
|
95
|
+
<ContentListTable
|
|
96
|
+
data={dataRetriever}
|
|
97
|
+
tableGeneratorType={mockModule as any}
|
|
98
|
+
fields={["id", "title"]}
|
|
99
|
+
title="Articles"
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(screen.getByText("Articles")).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should render table headers", () => {
|
|
107
|
+
const dataRetriever = createMockDataRetriever({
|
|
108
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
render(
|
|
112
|
+
<ContentListTable
|
|
113
|
+
data={dataRetriever}
|
|
114
|
+
tableGeneratorType={mockModule as any}
|
|
115
|
+
fields={["id", "title"]}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(screen.getByText("ID")).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should render table rows with data", () => {
|
|
124
|
+
const dataRetriever = createMockDataRetriever({
|
|
125
|
+
data: [
|
|
126
|
+
{ id: "1", title: "First Article" },
|
|
127
|
+
{ id: "2", title: "Second Article" },
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
render(
|
|
132
|
+
<ContentListTable
|
|
133
|
+
data={dataRetriever}
|
|
134
|
+
tableGeneratorType={mockModule as any}
|
|
135
|
+
fields={["id", "title"]}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(screen.getByText("1")).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByText("First Article")).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText("2")).toBeInTheDocument();
|
|
142
|
+
expect(screen.getByText("Second Article")).toBeInTheDocument();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should render 'No results' when data is empty", () => {
|
|
146
|
+
const dataRetriever = createMockDataRetriever({
|
|
147
|
+
data: [],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
render(
|
|
151
|
+
<ContentListTable
|
|
152
|
+
data={dataRetriever}
|
|
153
|
+
tableGeneratorType={mockModule as any}
|
|
154
|
+
fields={["id", "title"]}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(screen.getByText("No results.")).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("search", () => {
|
|
163
|
+
it("should render search component when allowSearch is true and title is set", () => {
|
|
164
|
+
const dataRetriever = createMockDataRetriever({
|
|
165
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
render(
|
|
169
|
+
<ContentListTable
|
|
170
|
+
data={dataRetriever}
|
|
171
|
+
tableGeneratorType={mockModule as any}
|
|
172
|
+
fields={["id", "title"]}
|
|
173
|
+
title="Articles"
|
|
174
|
+
allowSearch={true}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(screen.getByTestId("content-table-search")).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("pagination", () => {
|
|
183
|
+
it("should render pagination buttons when next or previous is available", () => {
|
|
184
|
+
const dataRetriever = createMockDataRetriever({
|
|
185
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
186
|
+
next: vi.fn(),
|
|
187
|
+
previous: vi.fn(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
render(
|
|
191
|
+
<ContentListTable
|
|
192
|
+
data={dataRetriever}
|
|
193
|
+
tableGeneratorType={mockModule as any}
|
|
194
|
+
fields={["id", "title"]}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Should have navigation buttons
|
|
199
|
+
const buttons = screen.getAllByRole("button");
|
|
200
|
+
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should disable previous button when no previous page", () => {
|
|
204
|
+
const dataRetriever = createMockDataRetriever({
|
|
205
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
206
|
+
next: vi.fn(),
|
|
207
|
+
previous: undefined,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
render(
|
|
211
|
+
<ContentListTable
|
|
212
|
+
data={dataRetriever}
|
|
213
|
+
tableGeneratorType={mockModule as any}
|
|
214
|
+
fields={["id", "title"]}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const buttons = screen.getAllByRole("button");
|
|
219
|
+
const previousButton = buttons[0];
|
|
220
|
+
expect(previousButton).toBeDisabled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should disable next button when no next page", () => {
|
|
224
|
+
const dataRetriever = createMockDataRetriever({
|
|
225
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
226
|
+
next: undefined,
|
|
227
|
+
previous: vi.fn(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
render(
|
|
231
|
+
<ContentListTable
|
|
232
|
+
data={dataRetriever}
|
|
233
|
+
tableGeneratorType={mockModule as any}
|
|
234
|
+
fields={["id", "title"]}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const buttons = screen.getAllByRole("button");
|
|
239
|
+
const nextButton = buttons[buttons.length - 1];
|
|
240
|
+
expect(nextButton).toBeDisabled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should call next when next button is clicked", async () => {
|
|
244
|
+
const user = userEvent.setup();
|
|
245
|
+
const nextFn = vi.fn();
|
|
246
|
+
|
|
247
|
+
const dataRetriever = createMockDataRetriever({
|
|
248
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
249
|
+
next: nextFn,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
render(
|
|
253
|
+
<ContentListTable
|
|
254
|
+
data={dataRetriever}
|
|
255
|
+
tableGeneratorType={mockModule as any}
|
|
256
|
+
fields={["id", "title"]}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const buttons = screen.getAllByRole("button");
|
|
261
|
+
const nextButton = buttons[buttons.length - 1];
|
|
262
|
+
await user.click(nextButton);
|
|
263
|
+
|
|
264
|
+
expect(nextFn).toHaveBeenCalledWith(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should call previous when previous button is clicked", async () => {
|
|
268
|
+
const user = userEvent.setup();
|
|
269
|
+
const previousFn = vi.fn();
|
|
270
|
+
|
|
271
|
+
const dataRetriever = createMockDataRetriever({
|
|
272
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
273
|
+
previous: previousFn,
|
|
274
|
+
next: vi.fn(), // Need both to show footer
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
render(
|
|
278
|
+
<ContentListTable
|
|
279
|
+
data={dataRetriever}
|
|
280
|
+
tableGeneratorType={mockModule as any}
|
|
281
|
+
fields={["id", "title"]}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const buttons = screen.getAllByRole("button");
|
|
286
|
+
const previousButton = buttons[0];
|
|
287
|
+
await user.click(previousButton);
|
|
288
|
+
|
|
289
|
+
expect(previousFn).toHaveBeenCalledWith(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should display page info when available", () => {
|
|
293
|
+
const dataRetriever = createMockDataRetriever({
|
|
294
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
295
|
+
next: vi.fn(),
|
|
296
|
+
pageInfo: {
|
|
297
|
+
startItem: 1,
|
|
298
|
+
endItem: 25,
|
|
299
|
+
pageSize: 25,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
render(
|
|
304
|
+
<ContentListTable
|
|
305
|
+
data={dataRetriever}
|
|
306
|
+
tableGeneratorType={mockModule as any}
|
|
307
|
+
fields={["id", "title"]}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(screen.getByText("1-25")).toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("functions and filters", () => {
|
|
316
|
+
it("should render functions when provided", () => {
|
|
317
|
+
const dataRetriever = createMockDataRetriever({
|
|
318
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
render(
|
|
322
|
+
<ContentListTable
|
|
323
|
+
data={dataRetriever}
|
|
324
|
+
tableGeneratorType={mockModule as any}
|
|
325
|
+
fields={["id", "title"]}
|
|
326
|
+
title="Articles"
|
|
327
|
+
functions={<button data-testid="custom-function">Custom</button>}
|
|
328
|
+
/>
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(screen.getByTestId("custom-function")).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should render filters when provided", () => {
|
|
335
|
+
const dataRetriever = createMockDataRetriever({
|
|
336
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
render(
|
|
340
|
+
<ContentListTable
|
|
341
|
+
data={dataRetriever}
|
|
342
|
+
tableGeneratorType={mockModule as any}
|
|
343
|
+
fields={["id", "title"]}
|
|
344
|
+
title="Articles"
|
|
345
|
+
filters={<div data-testid="custom-filter">Filter</div>}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(screen.getByTestId("custom-filter")).toBeInTheDocument();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("footer visibility", () => {
|
|
354
|
+
it("should show footer when functions are provided", () => {
|
|
355
|
+
const dataRetriever = createMockDataRetriever({
|
|
356
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
render(
|
|
360
|
+
<ContentListTable
|
|
361
|
+
data={dataRetriever}
|
|
362
|
+
tableGeneratorType={mockModule as any}
|
|
363
|
+
fields={["id", "title"]}
|
|
364
|
+
functions={<button>Action</button>}
|
|
365
|
+
/>
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Footer should be present with buttons
|
|
369
|
+
const buttons = screen.getAllByRole("button");
|
|
370
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should show footer when next page is available", () => {
|
|
374
|
+
const dataRetriever = createMockDataRetriever({
|
|
375
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
376
|
+
next: vi.fn(),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
render(
|
|
380
|
+
<ContentListTable
|
|
381
|
+
data={dataRetriever}
|
|
382
|
+
tableGeneratorType={mockModule as any}
|
|
383
|
+
fields={["id", "title"]}
|
|
384
|
+
/>
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Should have navigation buttons in footer
|
|
388
|
+
const buttons = screen.getAllByRole("button");
|
|
389
|
+
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should show footer when previous page is available", () => {
|
|
393
|
+
const dataRetriever = createMockDataRetriever({
|
|
394
|
+
data: [{ id: "1", title: "Article 1" }],
|
|
395
|
+
previous: vi.fn(),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
render(
|
|
399
|
+
<ContentListTable
|
|
400
|
+
data={dataRetriever}
|
|
401
|
+
tableGeneratorType={mockModule as any}
|
|
402
|
+
fields={["id", "title"]}
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Should have navigation buttons in footer
|
|
407
|
+
const buttons = screen.getAllByRole("button");
|
|
408
|
+
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EndpointCreator } from "../EndpointCreator";
|
|
3
|
+
import { createMockModule } from "../../../testing";
|
|
4
|
+
|
|
5
|
+
describe("EndpointCreator", () => {
|
|
6
|
+
describe("basic URL generation", () => {
|
|
7
|
+
it("should generate URL from string endpoint", () => {
|
|
8
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
9
|
+
expect(creator.generate()).toBe("articles");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should generate URL from module endpoint", () => {
|
|
13
|
+
const mockModule = createMockModule({ name: "articles" });
|
|
14
|
+
const creator = new EndpointCreator({ endpoint: mockModule });
|
|
15
|
+
expect(creator.generate()).toBe("articles");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should include id in URL", () => {
|
|
19
|
+
const creator = new EndpointCreator({ endpoint: "articles", id: "123" });
|
|
20
|
+
expect(creator.generate()).toBe("articles/123");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should include child endpoint", () => {
|
|
24
|
+
const creator = new EndpointCreator({
|
|
25
|
+
endpoint: "articles",
|
|
26
|
+
id: "123",
|
|
27
|
+
childEndpoint: "comments",
|
|
28
|
+
});
|
|
29
|
+
expect(creator.generate()).toBe("articles/123/comments");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should include child id", () => {
|
|
33
|
+
const creator = new EndpointCreator({
|
|
34
|
+
endpoint: "articles",
|
|
35
|
+
id: "123",
|
|
36
|
+
childEndpoint: "comments",
|
|
37
|
+
childId: "456",
|
|
38
|
+
});
|
|
39
|
+
expect(creator.generate()).toBe("articles/123/comments/456");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("fluent API", () => {
|
|
44
|
+
it("should support chaining endpoint()", () => {
|
|
45
|
+
const creator = new EndpointCreator({ endpoint: "initial" });
|
|
46
|
+
const result = creator.endpoint("articles");
|
|
47
|
+
expect(result).toBe(creator);
|
|
48
|
+
expect(creator.generate()).toBe("articles");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should support chaining id()", () => {
|
|
52
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
53
|
+
const result = creator.id("123");
|
|
54
|
+
expect(result).toBe(creator);
|
|
55
|
+
expect(creator.generate()).toBe("articles/123");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should support chaining childEndpoint()", () => {
|
|
59
|
+
const creator = new EndpointCreator({ endpoint: "articles", id: "123" });
|
|
60
|
+
const result = creator.childEndpoint("comments");
|
|
61
|
+
expect(result).toBe(creator);
|
|
62
|
+
expect(creator.generate()).toBe("articles/123/comments");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should support chaining childId()", () => {
|
|
66
|
+
const creator = new EndpointCreator({
|
|
67
|
+
endpoint: "articles",
|
|
68
|
+
id: "123",
|
|
69
|
+
childEndpoint: "comments",
|
|
70
|
+
});
|
|
71
|
+
const result = creator.childId("456");
|
|
72
|
+
expect(result).toBe(creator);
|
|
73
|
+
expect(creator.generate()).toBe("articles/123/comments/456");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should support full fluent chain", () => {
|
|
77
|
+
const creator = new EndpointCreator({ endpoint: "articles" })
|
|
78
|
+
.id("123")
|
|
79
|
+
.childEndpoint("comments")
|
|
80
|
+
.childId("456");
|
|
81
|
+
expect(creator.generate()).toBe("articles/123/comments/456");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("query parameters", () => {
|
|
86
|
+
it("should add additional params", () => {
|
|
87
|
+
const creator = new EndpointCreator({
|
|
88
|
+
endpoint: "articles",
|
|
89
|
+
additionalParams: [{ key: "page", value: "1" }],
|
|
90
|
+
});
|
|
91
|
+
expect(creator.generate()).toBe("articles?page=1");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should support addAdditionalParam()", () => {
|
|
95
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
96
|
+
creator.addAdditionalParam("page", "1");
|
|
97
|
+
creator.addAdditionalParam("limit", "10");
|
|
98
|
+
expect(creator.generate()).toBe("articles?page=1&limit=10");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should support array values in params", () => {
|
|
102
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
103
|
+
creator.addAdditionalParam("filter", ["tag1", "tag2"]);
|
|
104
|
+
expect(creator.generate()).toBe("articles?filter=tag1,tag2");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should combine URL and params", () => {
|
|
108
|
+
const creator = new EndpointCreator({
|
|
109
|
+
endpoint: "articles",
|
|
110
|
+
id: "123",
|
|
111
|
+
});
|
|
112
|
+
creator.addAdditionalParam("include", "author");
|
|
113
|
+
expect(creator.generate()).toBe("articles/123?include=author");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("limitToType", () => {
|
|
118
|
+
it("should add include param with single type", () => {
|
|
119
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
120
|
+
creator.limitToType(["author"]);
|
|
121
|
+
expect(creator.generate()).toBe("articles?include=author");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should add include param with multiple types", () => {
|
|
125
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
126
|
+
creator.limitToType(["author", "comments", "tags"]);
|
|
127
|
+
expect(creator.generate()).toBe("articles?include=author,comments,tags");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("limitToFields", () => {
|
|
132
|
+
it("should add fields param with single selector", () => {
|
|
133
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
134
|
+
creator.limitToFields([{ type: "articles", fields: ["title", "body"] }]);
|
|
135
|
+
expect(creator.generate()).toBe("articles?fields[articles]=title,body");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should add fields param with multiple selectors", () => {
|
|
139
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
140
|
+
creator.limitToFields([
|
|
141
|
+
{ type: "articles", fields: ["title", "body"] },
|
|
142
|
+
{ type: "authors", fields: ["name", "email"] },
|
|
143
|
+
]);
|
|
144
|
+
expect(creator.generate()).toBe(
|
|
145
|
+
"articles?fields[articles]=title,body&fields[authors]=name,email"
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should handle empty selectors array", () => {
|
|
150
|
+
const creator = new EndpointCreator({ endpoint: "articles" });
|
|
151
|
+
creator.limitToFields([]);
|
|
152
|
+
expect(creator.generate()).toBe("articles");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("module endpoint support", () => {
|
|
157
|
+
it("should work with module as child endpoint", () => {
|
|
158
|
+
const parentModule = createMockModule({ name: "users" });
|
|
159
|
+
const childModule = createMockModule({ name: "posts" });
|
|
160
|
+
const creator = new EndpointCreator({
|
|
161
|
+
endpoint: parentModule,
|
|
162
|
+
id: "1",
|
|
163
|
+
childEndpoint: childModule,
|
|
164
|
+
});
|
|
165
|
+
expect(creator.generate()).toBe("users/1/posts");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { JsonApiDataFactory } from "../JsonApiDataFactory";
|
|
3
|
+
import { DataClassRegistry } from "../../registry/DataClassRegistry";
|
|
4
|
+
import { ApiDataInterface } from "../../interfaces/ApiDataInterface";
|
|
5
|
+
|
|
6
|
+
// Mock class that tracks createJsonApi calls
|
|
7
|
+
class MockDataClass implements Partial<ApiDataInterface> {
|
|
8
|
+
type = "articles";
|
|
9
|
+
id = "1";
|
|
10
|
+
|
|
11
|
+
createJsonApi = vi.fn((data: any) => ({
|
|
12
|
+
type: "articles",
|
|
13
|
+
attributes: data,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
get included() {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
get createdAt() {
|
|
20
|
+
return new Date();
|
|
21
|
+
}
|
|
22
|
+
get updatedAt() {
|
|
23
|
+
return new Date();
|
|
24
|
+
}
|
|
25
|
+
get self() {
|
|
26
|
+
return "/articles/1";
|
|
27
|
+
}
|
|
28
|
+
get jsonApi() {
|
|
29
|
+
return { type: this.type, id: this.id };
|
|
30
|
+
}
|
|
31
|
+
generateApiUrl() {
|
|
32
|
+
return `/articles/${this.id}`;
|
|
33
|
+
}
|
|
34
|
+
dehydrate() {
|
|
35
|
+
return { jsonApi: this.jsonApi, included: [], allData: [] };
|
|
36
|
+
}
|
|
37
|
+
rehydrate(data: any) {
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("JsonApiDataFactory", () => {
|
|
43
|
+
const mockModule = { name: "articles", model: MockDataClass };
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
DataClassRegistry.clear();
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("create", () => {
|
|
51
|
+
it("should create JSON:API formatted data", () => {
|
|
52
|
+
DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
|
|
53
|
+
|
|
54
|
+
const inputData = { title: "Test Article", body: "Content" };
|
|
55
|
+
const result = JsonApiDataFactory.create(mockModule as any, inputData);
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual({
|
|
58
|
+
type: "articles",
|
|
59
|
+
attributes: inputData,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should use the registered class to create data", () => {
|
|
64
|
+
DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
|
|
65
|
+
|
|
66
|
+
const inputData = { title: "Test" };
|
|
67
|
+
JsonApiDataFactory.create(mockModule as any, inputData);
|
|
68
|
+
|
|
69
|
+
// The create method should have been called
|
|
70
|
+
// Note: We can't directly verify createJsonApi was called since
|
|
71
|
+
// it's called on a new instance, but we can verify the output format
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should throw for unregistered module", () => {
|
|
75
|
+
const unregisteredModule = { name: "unknown", model: MockDataClass };
|
|
76
|
+
|
|
77
|
+
expect(() =>
|
|
78
|
+
JsonApiDataFactory.create(unregisteredModule as any, { data: "test" })
|
|
79
|
+
).toThrow("Class not registered for key: unknown");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle empty data", () => {
|
|
83
|
+
DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
|
|
84
|
+
|
|
85
|
+
const result = JsonApiDataFactory.create(mockModule as any, {});
|
|
86
|
+
|
|
87
|
+
expect(result).toEqual({
|
|
88
|
+
type: "articles",
|
|
89
|
+
attributes: {},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle complex nested data", () => {
|
|
94
|
+
DataClassRegistry.registerObjectClass(mockModule as any, MockDataClass as any);
|
|
95
|
+
|
|
96
|
+
const complexData = {
|
|
97
|
+
title: "Test",
|
|
98
|
+
metadata: {
|
|
99
|
+
tags: ["tag1", "tag2"],
|
|
100
|
+
author: { name: "John", email: "john@example.com" },
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = JsonApiDataFactory.create(mockModule as any, complexData);
|
|
105
|
+
|
|
106
|
+
expect(result.attributes).toEqual(complexData);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|