@izumisy-tailor/tailor-data-viewer 0.1.21 → 0.1.23
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 +45 -1
- package/dist/generator/index.d.mts +54 -31
- package/dist/generator/index.mjs +20 -13
- package/docs/compositional-api.md +366 -0
- package/package.json +1 -1
- package/src/app-shell/create-data-view-module.tsx +1 -1
- package/src/component/column-selector.test.tsx +143 -103
- package/src/component/column-selector.tsx +121 -156
- package/src/component/contexts/data-viewer-context.test.tsx +191 -0
- package/src/component/contexts/data-viewer-context.tsx +244 -0
- package/src/component/contexts/index.ts +19 -0
- package/src/component/contexts/table-data-context.tsx +114 -0
- package/src/component/contexts/toolbar-context.tsx +62 -0
- package/src/component/csv-button.tsx +79 -0
- package/src/component/data-table-toolbar.test.tsx +127 -72
- package/src/component/data-table-toolbar.tsx +14 -151
- package/src/component/data-table.tsx +255 -225
- package/src/component/data-view-tab-content.tsx +68 -138
- package/src/component/data-viewer.tsx +11 -11
- package/src/component/hooks/use-column-state.ts +2 -2
- package/src/component/hooks/use-table-data.test.ts +399 -0
- package/src/component/hooks/use-table-data.ts +24 -7
- package/src/component/index.ts +43 -1
- package/src/component/refresh-button.tsx +20 -0
- package/src/component/saved-view-context.tsx +31 -2
- package/src/component/search-filter.test.tsx +612 -0
- package/src/component/search-filter.tsx +168 -33
- package/src/component/single-record-tab-content.test.tsx +10 -10
- package/src/component/single-record-tab-content.tsx +62 -21
- package/src/component/types.ts +78 -0
- package/src/component/view-save-load.tsx +13 -17
- package/src/generator/metadata-generator.ts +100 -67
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, within, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { SearchFilterForm } from "./search-filter";
|
|
5
|
+
import { DataViewerProvider } from "./contexts";
|
|
6
|
+
import { ToolbarProvider } from "./contexts";
|
|
7
|
+
import type {
|
|
8
|
+
FieldMetadata,
|
|
9
|
+
TableMetadata,
|
|
10
|
+
TableMetadataMap,
|
|
11
|
+
} from "../generator/metadata-generator";
|
|
12
|
+
import type { ReactNode } from "react";
|
|
13
|
+
import type { SearchFilters } from "./types";
|
|
14
|
+
|
|
15
|
+
// Mock fields covering all filterable types
|
|
16
|
+
const mockFields: FieldMetadata[] = [
|
|
17
|
+
{ name: "id", type: "uuid", required: true, description: "ID" },
|
|
18
|
+
{ name: "name", type: "string", required: true, description: "名前" },
|
|
19
|
+
{ name: "email", type: "string", required: false, description: "メール" },
|
|
20
|
+
{ name: "age", type: "number", required: false, description: "年齢" },
|
|
21
|
+
{ name: "isActive", type: "boolean", required: false, description: "有効" },
|
|
22
|
+
{
|
|
23
|
+
name: "status",
|
|
24
|
+
type: "enum",
|
|
25
|
+
required: false,
|
|
26
|
+
description: "ステータス",
|
|
27
|
+
enumValues: ["ACTIVE", "INACTIVE", "PENDING"],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "createdAt",
|
|
31
|
+
type: "datetime",
|
|
32
|
+
required: false,
|
|
33
|
+
description: "作成日時",
|
|
34
|
+
},
|
|
35
|
+
{ name: "birthDate", type: "date", required: false, description: "生年月日" },
|
|
36
|
+
// Non-filterable types
|
|
37
|
+
{ name: "tags", type: "array", required: false, description: "タグ" },
|
|
38
|
+
{
|
|
39
|
+
name: "metadata",
|
|
40
|
+
type: "nested",
|
|
41
|
+
required: false,
|
|
42
|
+
description: "メタデータ",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const mockTableMetadata: TableMetadata = {
|
|
47
|
+
name: "TestTable",
|
|
48
|
+
pluralForm: "TestTables",
|
|
49
|
+
readAllowedRoles: [],
|
|
50
|
+
fields: mockFields,
|
|
51
|
+
relations: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const mockTableMetadataMap: TableMetadataMap = {
|
|
55
|
+
TestTable: mockTableMetadata,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
interface WrapperProps {
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
initialFilters?: SearchFilters;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function TestWrapper({ children, initialFilters = [] }: WrapperProps) {
|
|
64
|
+
return (
|
|
65
|
+
<DataViewerProvider
|
|
66
|
+
appUri="https://example.com"
|
|
67
|
+
tableName={mockTableMetadata.name}
|
|
68
|
+
metadata={mockTableMetadataMap}
|
|
69
|
+
initialData={{
|
|
70
|
+
selectedFields: ["id", "name"],
|
|
71
|
+
filters: initialFilters,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<ToolbarProvider>{children}</ToolbarProvider>
|
|
75
|
+
</DataViewerProvider>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("SearchFilterForm", () => {
|
|
80
|
+
describe("基本的な表示", () => {
|
|
81
|
+
it("検索ボタンが表示される", () => {
|
|
82
|
+
render(
|
|
83
|
+
<TestWrapper>
|
|
84
|
+
<SearchFilterForm />
|
|
85
|
+
</TestWrapper>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByRole("button", { name: /検索/ })).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("フィルターがない場合はバッジが表示されない", () => {
|
|
92
|
+
render(
|
|
93
|
+
<TestWrapper>
|
|
94
|
+
<SearchFilterForm />
|
|
95
|
+
</TestWrapper>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const button = screen.getByRole("button", { name: /検索/ });
|
|
99
|
+
expect(within(button).queryByText("1")).not.toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("フィルターがある場合はバッジにフィルター数が表示される", () => {
|
|
103
|
+
render(
|
|
104
|
+
<TestWrapper
|
|
105
|
+
initialFilters={[
|
|
106
|
+
{
|
|
107
|
+
field: "name",
|
|
108
|
+
fieldType: "string",
|
|
109
|
+
operator: "eq",
|
|
110
|
+
value: "test",
|
|
111
|
+
},
|
|
112
|
+
]}
|
|
113
|
+
>
|
|
114
|
+
<SearchFilterForm />
|
|
115
|
+
</TestWrapper>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const button = screen.getByRole("button", { name: /検索/ });
|
|
119
|
+
expect(within(button).getByText("1")).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("複数フィルターがある場合は正しいフィルター数が表示される", () => {
|
|
123
|
+
render(
|
|
124
|
+
<TestWrapper
|
|
125
|
+
initialFilters={[
|
|
126
|
+
{
|
|
127
|
+
field: "name",
|
|
128
|
+
fieldType: "string",
|
|
129
|
+
operator: "eq",
|
|
130
|
+
value: "test",
|
|
131
|
+
},
|
|
132
|
+
{ field: "age", fieldType: "number", operator: "gt", value: "18" },
|
|
133
|
+
{
|
|
134
|
+
field: "isActive",
|
|
135
|
+
fieldType: "boolean",
|
|
136
|
+
operator: "eq",
|
|
137
|
+
value: true,
|
|
138
|
+
},
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
<SearchFilterForm />
|
|
142
|
+
</TestWrapper>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const button = screen.getByRole("button", { name: /検索/ });
|
|
146
|
+
expect(within(button).getByText("3")).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("ドロップダウンメニュー", () => {
|
|
151
|
+
it("ボタンクリックでドロップダウンが開く", async () => {
|
|
152
|
+
const user = userEvent.setup();
|
|
153
|
+
render(
|
|
154
|
+
<TestWrapper>
|
|
155
|
+
<SearchFilterForm />
|
|
156
|
+
</TestWrapper>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
160
|
+
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
const body = within(document.body);
|
|
163
|
+
expect(body.getByText("検索フィルター")).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("フィルターを追加セクションが表示される", async () => {
|
|
168
|
+
const user = userEvent.setup();
|
|
169
|
+
render(
|
|
170
|
+
<TestWrapper>
|
|
171
|
+
<SearchFilterForm />
|
|
172
|
+
</TestWrapper>,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
176
|
+
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
const body = within(document.body);
|
|
179
|
+
expect(body.getByText("フィルターを追加")).toBeInTheDocument();
|
|
180
|
+
expect(body.getByText("フィールドを選択")).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("フィルターの表示", () => {
|
|
186
|
+
it("適用中のフィルターが表示される", async () => {
|
|
187
|
+
const user = userEvent.setup();
|
|
188
|
+
render(
|
|
189
|
+
<TestWrapper
|
|
190
|
+
initialFilters={[
|
|
191
|
+
{
|
|
192
|
+
field: "name",
|
|
193
|
+
fieldType: "string",
|
|
194
|
+
operator: "eq",
|
|
195
|
+
value: "test",
|
|
196
|
+
},
|
|
197
|
+
]}
|
|
198
|
+
>
|
|
199
|
+
<SearchFilterForm />
|
|
200
|
+
</TestWrapper>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
const body = within(document.body);
|
|
207
|
+
expect(body.getByText("適用中のフィルター (AND)")).toBeInTheDocument();
|
|
208
|
+
expect(body.getByText("name = test")).toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("すべてクリアボタンが表示される", async () => {
|
|
213
|
+
const user = userEvent.setup();
|
|
214
|
+
render(
|
|
215
|
+
<TestWrapper
|
|
216
|
+
initialFilters={[
|
|
217
|
+
{
|
|
218
|
+
field: "name",
|
|
219
|
+
fieldType: "string",
|
|
220
|
+
operator: "eq",
|
|
221
|
+
value: "test",
|
|
222
|
+
},
|
|
223
|
+
]}
|
|
224
|
+
>
|
|
225
|
+
<SearchFilterForm />
|
|
226
|
+
</TestWrapper>,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
230
|
+
|
|
231
|
+
await waitFor(() => {
|
|
232
|
+
const body = within(document.body);
|
|
233
|
+
expect(body.getByText("すべてクリア")).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("フィルターがない場合はすべてクリアボタンが表示されない", async () => {
|
|
238
|
+
const user = userEvent.setup();
|
|
239
|
+
render(
|
|
240
|
+
<TestWrapper>
|
|
241
|
+
<SearchFilterForm />
|
|
242
|
+
</TestWrapper>,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
246
|
+
|
|
247
|
+
await waitFor(() => {
|
|
248
|
+
const body = within(document.body);
|
|
249
|
+
expect(body.getByText("検索フィルター")).toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const body = within(document.body);
|
|
253
|
+
expect(body.queryByText("すべてクリア")).not.toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("フィルター表示形式", () => {
|
|
258
|
+
it("eq 演算子は「=」で表示される", async () => {
|
|
259
|
+
render(
|
|
260
|
+
<TestWrapper
|
|
261
|
+
initialFilters={[
|
|
262
|
+
{
|
|
263
|
+
field: "name",
|
|
264
|
+
fieldType: "string",
|
|
265
|
+
operator: "eq",
|
|
266
|
+
value: "test",
|
|
267
|
+
},
|
|
268
|
+
]}
|
|
269
|
+
>
|
|
270
|
+
<SearchFilterForm />
|
|
271
|
+
</TestWrapper>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const user = userEvent.setup();
|
|
275
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
276
|
+
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
const body = within(document.body);
|
|
279
|
+
expect(body.getByText("name = test")).toBeInTheDocument();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("gt 演算子は「>」で表示される", async () => {
|
|
284
|
+
render(
|
|
285
|
+
<TestWrapper
|
|
286
|
+
initialFilters={[
|
|
287
|
+
{ field: "age", fieldType: "number", operator: "gt", value: "18" },
|
|
288
|
+
]}
|
|
289
|
+
>
|
|
290
|
+
<SearchFilterForm />
|
|
291
|
+
</TestWrapper>,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const user = userEvent.setup();
|
|
295
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
296
|
+
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
const body = within(document.body);
|
|
299
|
+
expect(body.getByText("age > 18")).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("lt 演算子は「<」で表示される", async () => {
|
|
304
|
+
render(
|
|
305
|
+
<TestWrapper
|
|
306
|
+
initialFilters={[
|
|
307
|
+
{ field: "age", fieldType: "number", operator: "lt", value: "65" },
|
|
308
|
+
]}
|
|
309
|
+
>
|
|
310
|
+
<SearchFilterForm />
|
|
311
|
+
</TestWrapper>,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const user = userEvent.setup();
|
|
315
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
316
|
+
|
|
317
|
+
await waitFor(() => {
|
|
318
|
+
const body = within(document.body);
|
|
319
|
+
expect(body.getByText("age < 65")).toBeInTheDocument();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("contains 演算子は「含む」で表示される", async () => {
|
|
324
|
+
render(
|
|
325
|
+
<TestWrapper
|
|
326
|
+
initialFilters={[
|
|
327
|
+
{
|
|
328
|
+
field: "name",
|
|
329
|
+
fieldType: "string",
|
|
330
|
+
operator: "contains",
|
|
331
|
+
value: "test",
|
|
332
|
+
},
|
|
333
|
+
]}
|
|
334
|
+
>
|
|
335
|
+
<SearchFilterForm />
|
|
336
|
+
</TestWrapper>,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const user = userEvent.setup();
|
|
340
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
341
|
+
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
const body = within(document.body);
|
|
344
|
+
expect(body.getByText('name 含む "test"')).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("hasPrefix 演算子は「で始まる」で表示される", async () => {
|
|
349
|
+
render(
|
|
350
|
+
<TestWrapper
|
|
351
|
+
initialFilters={[
|
|
352
|
+
{
|
|
353
|
+
field: "name",
|
|
354
|
+
fieldType: "string",
|
|
355
|
+
operator: "hasPrefix",
|
|
356
|
+
value: "pre",
|
|
357
|
+
},
|
|
358
|
+
]}
|
|
359
|
+
>
|
|
360
|
+
<SearchFilterForm />
|
|
361
|
+
</TestWrapper>,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const user = userEvent.setup();
|
|
365
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
366
|
+
|
|
367
|
+
await waitFor(() => {
|
|
368
|
+
const body = within(document.body);
|
|
369
|
+
expect(body.getByText('name "pre" で始まる')).toBeInTheDocument();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("hasSuffix 演算子は「で終わる」で表示される", async () => {
|
|
374
|
+
render(
|
|
375
|
+
<TestWrapper
|
|
376
|
+
initialFilters={[
|
|
377
|
+
{
|
|
378
|
+
field: "name",
|
|
379
|
+
fieldType: "string",
|
|
380
|
+
operator: "hasSuffix",
|
|
381
|
+
value: "suf",
|
|
382
|
+
},
|
|
383
|
+
]}
|
|
384
|
+
>
|
|
385
|
+
<SearchFilterForm />
|
|
386
|
+
</TestWrapper>,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const user = userEvent.setup();
|
|
390
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
391
|
+
|
|
392
|
+
await waitFor(() => {
|
|
393
|
+
const body = within(document.body);
|
|
394
|
+
expect(body.getByText('name "suf" で終わる')).toBeInTheDocument();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("boolean true は「true」で表示される", async () => {
|
|
399
|
+
render(
|
|
400
|
+
<TestWrapper
|
|
401
|
+
initialFilters={[
|
|
402
|
+
{
|
|
403
|
+
field: "isActive",
|
|
404
|
+
fieldType: "boolean",
|
|
405
|
+
operator: "eq",
|
|
406
|
+
value: true,
|
|
407
|
+
},
|
|
408
|
+
]}
|
|
409
|
+
>
|
|
410
|
+
<SearchFilterForm />
|
|
411
|
+
</TestWrapper>,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const user = userEvent.setup();
|
|
415
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
416
|
+
|
|
417
|
+
await waitFor(() => {
|
|
418
|
+
const body = within(document.body);
|
|
419
|
+
expect(body.getByText("isActive = true")).toBeInTheDocument();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("boolean false は「false」で表示される", async () => {
|
|
424
|
+
render(
|
|
425
|
+
<TestWrapper
|
|
426
|
+
initialFilters={[
|
|
427
|
+
{
|
|
428
|
+
field: "isActive",
|
|
429
|
+
fieldType: "boolean",
|
|
430
|
+
operator: "eq",
|
|
431
|
+
value: false,
|
|
432
|
+
},
|
|
433
|
+
]}
|
|
434
|
+
>
|
|
435
|
+
<SearchFilterForm />
|
|
436
|
+
</TestWrapper>,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const user = userEvent.setup();
|
|
440
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
441
|
+
|
|
442
|
+
await waitFor(() => {
|
|
443
|
+
const body = within(document.body);
|
|
444
|
+
expect(body.getByText("isActive = false")).toBeInTheDocument();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("日時フィルターの値が表示される", async () => {
|
|
449
|
+
render(
|
|
450
|
+
<TestWrapper
|
|
451
|
+
initialFilters={[
|
|
452
|
+
{
|
|
453
|
+
field: "createdAt",
|
|
454
|
+
fieldType: "datetime",
|
|
455
|
+
operator: "gt",
|
|
456
|
+
value: "2024-01-01T00:00",
|
|
457
|
+
},
|
|
458
|
+
]}
|
|
459
|
+
>
|
|
460
|
+
<SearchFilterForm />
|
|
461
|
+
</TestWrapper>,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const user = userEvent.setup();
|
|
465
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
466
|
+
|
|
467
|
+
await waitFor(() => {
|
|
468
|
+
const body = within(document.body);
|
|
469
|
+
expect(
|
|
470
|
+
body.getByText("createdAt > 2024-01-01T00:00"),
|
|
471
|
+
).toBeInTheDocument();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("日付フィルターの値が表示される", async () => {
|
|
476
|
+
render(
|
|
477
|
+
<TestWrapper
|
|
478
|
+
initialFilters={[
|
|
479
|
+
{
|
|
480
|
+
field: "birthDate",
|
|
481
|
+
fieldType: "date",
|
|
482
|
+
operator: "lt",
|
|
483
|
+
value: "2000-01-01",
|
|
484
|
+
},
|
|
485
|
+
]}
|
|
486
|
+
>
|
|
487
|
+
<SearchFilterForm />
|
|
488
|
+
</TestWrapper>,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const user = userEvent.setup();
|
|
492
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
493
|
+
|
|
494
|
+
await waitFor(() => {
|
|
495
|
+
const body = within(document.body);
|
|
496
|
+
expect(body.getByText("birthDate < 2000-01-01")).toBeInTheDocument();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe("フィルターの削除", () => {
|
|
502
|
+
it("すべてクリアボタンで全フィルターを削除できる", async () => {
|
|
503
|
+
const user = userEvent.setup();
|
|
504
|
+
render(
|
|
505
|
+
<TestWrapper
|
|
506
|
+
initialFilters={[
|
|
507
|
+
{
|
|
508
|
+
field: "name",
|
|
509
|
+
fieldType: "string",
|
|
510
|
+
operator: "eq",
|
|
511
|
+
value: "test",
|
|
512
|
+
},
|
|
513
|
+
{ field: "age", fieldType: "number", operator: "eq", value: "25" },
|
|
514
|
+
]}
|
|
515
|
+
>
|
|
516
|
+
<SearchFilterForm />
|
|
517
|
+
</TestWrapper>,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
521
|
+
|
|
522
|
+
const body = within(document.body);
|
|
523
|
+
await user.click(body.getByText("すべてクリア"));
|
|
524
|
+
|
|
525
|
+
// Verify all filters are removed
|
|
526
|
+
await waitFor(() => {
|
|
527
|
+
expect(body.queryByText(/name = test/)).not.toBeInTheDocument();
|
|
528
|
+
expect(body.queryByText(/age = 25/)).not.toBeInTheDocument();
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("個別のフィルターを削除できる", async () => {
|
|
533
|
+
const user = userEvent.setup();
|
|
534
|
+
render(
|
|
535
|
+
<TestWrapper
|
|
536
|
+
initialFilters={[
|
|
537
|
+
{
|
|
538
|
+
field: "name",
|
|
539
|
+
fieldType: "string",
|
|
540
|
+
operator: "eq",
|
|
541
|
+
value: "test",
|
|
542
|
+
},
|
|
543
|
+
{ field: "age", fieldType: "number", operator: "eq", value: "25" },
|
|
544
|
+
]}
|
|
545
|
+
>
|
|
546
|
+
<SearchFilterForm />
|
|
547
|
+
</TestWrapper>,
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
551
|
+
|
|
552
|
+
const body = within(document.body);
|
|
553
|
+
|
|
554
|
+
// Find the badge containing "name = test" and get the button within it
|
|
555
|
+
const nameBadge = body
|
|
556
|
+
.getByText(/name = test/)
|
|
557
|
+
.closest("span[data-slot='badge']") as HTMLElement;
|
|
558
|
+
expect(nameBadge).not.toBeNull();
|
|
559
|
+
const removeButton = within(nameBadge).getByRole("button");
|
|
560
|
+
await user.click(removeButton);
|
|
561
|
+
|
|
562
|
+
// Verify only the age filter remains
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(body.queryByText(/name = test/)).not.toBeInTheDocument();
|
|
565
|
+
expect(body.getByText(/age = 25/)).toBeInTheDocument();
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("複数フィルター表示", () => {
|
|
571
|
+
it("複数のフィルターがすべて表示される", async () => {
|
|
572
|
+
const user = userEvent.setup();
|
|
573
|
+
render(
|
|
574
|
+
<TestWrapper
|
|
575
|
+
initialFilters={[
|
|
576
|
+
{
|
|
577
|
+
field: "name",
|
|
578
|
+
fieldType: "string",
|
|
579
|
+
operator: "contains",
|
|
580
|
+
value: "test",
|
|
581
|
+
},
|
|
582
|
+
{ field: "age", fieldType: "number", operator: "gt", value: "18" },
|
|
583
|
+
{
|
|
584
|
+
field: "isActive",
|
|
585
|
+
fieldType: "boolean",
|
|
586
|
+
operator: "eq",
|
|
587
|
+
value: true,
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
field: "status",
|
|
591
|
+
fieldType: "enum",
|
|
592
|
+
operator: "eq",
|
|
593
|
+
value: "ACTIVE",
|
|
594
|
+
},
|
|
595
|
+
]}
|
|
596
|
+
>
|
|
597
|
+
<SearchFilterForm />
|
|
598
|
+
</TestWrapper>,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
await user.click(screen.getByRole("button", { name: /検索/ }));
|
|
602
|
+
|
|
603
|
+
await waitFor(() => {
|
|
604
|
+
const body = within(document.body);
|
|
605
|
+
expect(body.getByText('name 含む "test"')).toBeInTheDocument();
|
|
606
|
+
expect(body.getByText("age > 18")).toBeInTheDocument();
|
|
607
|
+
expect(body.getByText("isActive = true")).toBeInTheDocument();
|
|
608
|
+
expect(body.getByText("status = ACTIVE")).toBeInTheDocument();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|