@truedat/core 8.13.0 → 8.13.2
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/package.json +3 -3
- package/src/components/UploadJob.js +102 -178
- package/src/components/UploadJobBreadcrumbs.js +13 -17
- package/src/components/UploadJobDetails.js +256 -0
- package/src/components/UploadJobHistory.js +90 -0
- package/src/components/UploadJobParser.js +155 -89
- package/src/components/UploadJobResults.js +278 -0
- package/src/components/UploadJobScope.js +37 -0
- package/src/components/UploadJobSummary.js +211 -0
- package/src/components/UploadJobs.js +48 -17
- package/src/components/__tests__/UploadJob.spec.js +118 -42
- package/src/components/__tests__/UploadJobBreadcrumbs.spec.js +3 -0
- package/src/components/__tests__/UploadJobHistory.spec.js +71 -0
- package/src/components/__tests__/UploadJobParser.spec.js +560 -26
- package/src/components/__tests__/UploadJobResults.spec.js +289 -0
- package/src/components/__tests__/UploadJobScope.spec.js +38 -0
- package/src/components/__tests__/UploadJobSummary.spec.js +208 -0
- package/src/components/__tests__/UploadJobs.spec.js +133 -1
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
import userEvent from "@testing-library/user-event";
|
|
2
2
|
import { renderWithIntl as render } from "@truedat/test/renderWithIntl";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ResultDetailCell,
|
|
5
|
+
ResultEntityCell,
|
|
6
|
+
StatusPill,
|
|
7
|
+
ResponseCell,
|
|
8
|
+
} from "../UploadJobParser";
|
|
4
9
|
|
|
5
|
-
jest.mock("@truedat/dq/components/RuleImplementationLink", () => (
|
|
6
|
-
|
|
10
|
+
jest.mock("@truedat/dq/components/RuleImplementationLink", () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
default: ({ implementation_key, implementation_ref }) => (
|
|
13
|
+
<>
|
|
14
|
+
<a href={`/implementations/${implementation_key}`}>
|
|
15
|
+
{implementation_key}
|
|
16
|
+
</a>
|
|
17
|
+
{implementation_ref != null && implementation_ref !== "" ? (
|
|
18
|
+
<small>{implementation_ref}</small>
|
|
19
|
+
) : null}
|
|
20
|
+
</>
|
|
21
|
+
),
|
|
22
|
+
}));
|
|
23
|
+
jest.mock("@truedat/dd/components/StructureNoteLink", () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
default: ({ alias, external_id, name }) => {
|
|
26
|
+
const label = alias?.trim() || name || external_id;
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<a href={`/notes/${external_id}`}>{label}</a>
|
|
30
|
+
{external_id !== label ? <small>{external_id}</small> : null}
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const changeMessages = {
|
|
37
|
+
en: {
|
|
38
|
+
"uploadJob.parser.info.implementation_updated": "Item updated",
|
|
39
|
+
"uploadJob.parser.info.updated": "Updated",
|
|
40
|
+
"uploadJob.results.changes.modifiedFields":
|
|
41
|
+
"{count, plural, one {# modified field} other {# modified fields}}",
|
|
42
|
+
"uploadJob.results.changes.field": "Field",
|
|
43
|
+
"uploadJob.results.changes.previous": "Previous value",
|
|
44
|
+
"uploadJob.results.changes.change": "Change",
|
|
45
|
+
"uploadJob.results.changes.new": "New value",
|
|
46
|
+
"uploadJob.results.errors.errorFields":
|
|
47
|
+
"{count, plural, one {# field with an error} other {# fields with errors}}",
|
|
48
|
+
"uploadJob.results.errors.field": "Field",
|
|
49
|
+
"uploadJob.results.errors.error": "Error",
|
|
50
|
+
"uploadJob.parser.error.field_validation_error": "Field validation error",
|
|
51
|
+
"uploadJob.parser.error.data_structure_not_found":
|
|
52
|
+
"Data structure not found",
|
|
53
|
+
"uploadJob.parser.detail.message": "Message",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
7
56
|
|
|
8
57
|
describe("<StatusPill />", () => {
|
|
9
58
|
it("renders with correct color based on status", async () => {
|
|
@@ -23,6 +72,174 @@ describe("<StatusPill />", () => {
|
|
|
23
72
|
});
|
|
24
73
|
});
|
|
25
74
|
|
|
75
|
+
describe("<ResultEntityCell />", () => {
|
|
76
|
+
it("renders INFO implementation and structure-note links from row details", () => {
|
|
77
|
+
const implementation = render(
|
|
78
|
+
<ResultEntityCell
|
|
79
|
+
status="INFO"
|
|
80
|
+
response={{ details: { implementation_key: "impl" } }}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
expect(implementation.getByRole("link", { name: "impl" })).toHaveAttribute(
|
|
84
|
+
"href",
|
|
85
|
+
"/implementations/impl",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const note = render(
|
|
89
|
+
<ResultEntityCell
|
|
90
|
+
status="INFO"
|
|
91
|
+
response={{
|
|
92
|
+
details: { data_structure_id: 1, external_id: "note" },
|
|
93
|
+
}}
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
expect(note.getByRole("link", { name: "note" })).toHaveAttribute(
|
|
97
|
+
"href",
|
|
98
|
+
"/notes/note",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("renders implementation refs and preferred structure labels with external ids", () => {
|
|
103
|
+
const implementation = render(
|
|
104
|
+
<ResultEntityCell
|
|
105
|
+
status="INFO"
|
|
106
|
+
response={{
|
|
107
|
+
details: { implementation_key: "impl", implementation_ref: 0 },
|
|
108
|
+
}}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(implementation.getByText("0")).toBeInTheDocument();
|
|
112
|
+
|
|
113
|
+
const alias = render(
|
|
114
|
+
<ResultEntityCell
|
|
115
|
+
status="INFO"
|
|
116
|
+
response={{
|
|
117
|
+
details: {
|
|
118
|
+
alias: "Structure alias",
|
|
119
|
+
data_structure_id: 1,
|
|
120
|
+
external_id: "EXT-001",
|
|
121
|
+
name: "Structure name",
|
|
122
|
+
},
|
|
123
|
+
}}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
alias.getByRole("link", { name: "Structure alias" }),
|
|
128
|
+
).toHaveAttribute("href", "/notes/EXT-001");
|
|
129
|
+
expect(alias.getByText("EXT-001")).toBeInTheDocument();
|
|
130
|
+
|
|
131
|
+
const name = render(
|
|
132
|
+
<ResultEntityCell
|
|
133
|
+
status="INFO"
|
|
134
|
+
response={{
|
|
135
|
+
details: {
|
|
136
|
+
alias: " ",
|
|
137
|
+
data_structure_id: 1,
|
|
138
|
+
external_id: "EXT-002",
|
|
139
|
+
name: "Structure name",
|
|
140
|
+
},
|
|
141
|
+
}}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
expect(name.getByRole("link", { name: "Structure name" })).toHaveAttribute(
|
|
145
|
+
"href",
|
|
146
|
+
"/notes/EXT-002",
|
|
147
|
+
);
|
|
148
|
+
expect(name.getByText("EXT-002")).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders a structure-note link for errors with identified structures", () => {
|
|
152
|
+
const rendered = render(
|
|
153
|
+
<ResultEntityCell
|
|
154
|
+
status="ERROR"
|
|
155
|
+
response={{
|
|
156
|
+
type: "field_validation_error",
|
|
157
|
+
details: { data_structure_id: 1, external_id: "note" },
|
|
158
|
+
}}
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(rendered.getByRole("link", { name: "note" })).toHaveAttribute(
|
|
163
|
+
"href",
|
|
164
|
+
"/notes/note",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("renders an implementation link for errors with identified implementations", () => {
|
|
169
|
+
const rendered = render(
|
|
170
|
+
<ResultEntityCell
|
|
171
|
+
status="ERROR"
|
|
172
|
+
response={{ details: { implementation_key: "impl" } }}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(rendered.getByRole("link", { name: "impl" })).toHaveAttribute(
|
|
177
|
+
"href",
|
|
178
|
+
"/implementations/impl",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("does not render an entity for data_structure_not_found errors", () => {
|
|
183
|
+
const rendered = render(
|
|
184
|
+
<ResultEntityCell
|
|
185
|
+
status="ERROR"
|
|
186
|
+
response={{
|
|
187
|
+
type: "data_structure_not_found",
|
|
188
|
+
details: {
|
|
189
|
+
data_structure_id: 1,
|
|
190
|
+
external_id: "unknown-note",
|
|
191
|
+
implementation_key: "impl",
|
|
192
|
+
},
|
|
193
|
+
}}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(rendered.container.firstChild).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it.each([{ data_structure_id: 1 }, { external_id: "note" }])(
|
|
201
|
+
"does not render ERROR entities with incomplete structure identifiers",
|
|
202
|
+
(details) => {
|
|
203
|
+
const rendered = render(
|
|
204
|
+
<ResultEntityCell
|
|
205
|
+
status="ERROR"
|
|
206
|
+
response={{ type: "field_validation_error", details }}
|
|
207
|
+
/>,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(rendered.container.firstChild).toBeNull();
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
it("does not render an entity for errors without reliable identifiers", () => {
|
|
215
|
+
const rendered = render(
|
|
216
|
+
<ResultEntityCell
|
|
217
|
+
status="ERROR"
|
|
218
|
+
response={{ type: "field_validation_error", details: {} }}
|
|
219
|
+
/>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(rendered.container.firstChild).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("does not render an entity for non-result statuses", () => {
|
|
226
|
+
const rendered = render(
|
|
227
|
+
<ResultEntityCell
|
|
228
|
+
status="FAILED"
|
|
229
|
+
response={{
|
|
230
|
+
details: {
|
|
231
|
+
data_structure_id: 1,
|
|
232
|
+
external_id: "note",
|
|
233
|
+
implementation_key: "impl",
|
|
234
|
+
},
|
|
235
|
+
}}
|
|
236
|
+
/>,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(rendered.container.firstChild).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
26
243
|
describe("<ResponseCell />", () => {
|
|
27
244
|
it("returns null when response is empty", () => {
|
|
28
245
|
const rendered = render(
|
|
@@ -34,12 +251,66 @@ describe("<ResponseCell />", () => {
|
|
|
34
251
|
expect(rendered2.container.firstChild).toBeNull();
|
|
35
252
|
});
|
|
36
253
|
|
|
37
|
-
it("renders FAILED status with
|
|
38
|
-
const response = { message: "
|
|
254
|
+
it("renders FAILED status with a translated error code", () => {
|
|
255
|
+
const response = { message: "invalid_format" };
|
|
39
256
|
const rendered = render(
|
|
40
257
|
<ResponseCell response={response} status="FAILED" />,
|
|
258
|
+
{
|
|
259
|
+
messages: {
|
|
260
|
+
en: {
|
|
261
|
+
"uploadJob.parser.error.invalid_format": "Invalid file format",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
expect(rendered.getByText("Invalid file format")).toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("replaces FAILED technical exception messages with an unexpected error", () => {
|
|
270
|
+
const technicalMessage = '%RuntimeError{message: "boom"}';
|
|
271
|
+
const rendered = render(
|
|
272
|
+
<ResponseCell response={{ message: technicalMessage }} status="FAILED" />,
|
|
273
|
+
{
|
|
274
|
+
messages: {
|
|
275
|
+
en: {
|
|
276
|
+
"uploadJob.parser.error.unexpected_error": "Unexpected error",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(rendered.getByText("Unexpected error")).toBeInTheDocument();
|
|
283
|
+
expect(rendered.queryByText(technicalMessage)).not.toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("preserves FAILED error codes and human messages", () => {
|
|
287
|
+
const code = render(
|
|
288
|
+
<ResponseCell
|
|
289
|
+
response={{ message: "missing_required_headers" }}
|
|
290
|
+
status="FAILED"
|
|
291
|
+
/>,
|
|
292
|
+
);
|
|
293
|
+
expect(code.getByText(/missing_required_headers/i)).toBeInTheDocument();
|
|
294
|
+
|
|
295
|
+
const human = render(
|
|
296
|
+
<ResponseCell
|
|
297
|
+
response={{ message: "The file could not be processed" }}
|
|
298
|
+
status="FAILED"
|
|
299
|
+
/>,
|
|
300
|
+
);
|
|
301
|
+
expect(
|
|
302
|
+
human.getByText("The file could not be processed"),
|
|
303
|
+
).toBeInTheDocument();
|
|
304
|
+
|
|
305
|
+
const multiline = render(
|
|
306
|
+
<ResponseCell
|
|
307
|
+
response={{ message: "The file could not be processed\nPlease retry" }}
|
|
308
|
+
status="FAILED"
|
|
309
|
+
/>,
|
|
310
|
+
);
|
|
311
|
+
expect(multiline.container).toHaveTextContent(
|
|
312
|
+
"The file could not be processed Please retry",
|
|
41
313
|
);
|
|
42
|
-
expect(rendered.getByText(/missing_required_headers/i)).toBeInTheDocument();
|
|
43
314
|
});
|
|
44
315
|
|
|
45
316
|
it("renders ERROR status with details", () => {
|
|
@@ -85,10 +356,13 @@ describe("<ResponseCell />", () => {
|
|
|
85
356
|
},
|
|
86
357
|
},
|
|
87
358
|
};
|
|
88
|
-
const rendered = render(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
359
|
+
const rendered = render(
|
|
360
|
+
<ResponseCell response={response} status="INFO" />,
|
|
361
|
+
{
|
|
362
|
+
messages: changeMessages,
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
expect(rendered.getByText("Item updated")).toBeInTheDocument();
|
|
92
366
|
});
|
|
93
367
|
|
|
94
368
|
it("renders INFO status with deprecated_implementation specifically when unchanged and deprecated", () => {
|
|
@@ -99,7 +373,12 @@ describe("<ResponseCell />", () => {
|
|
|
99
373
|
is_deprecated: true,
|
|
100
374
|
},
|
|
101
375
|
};
|
|
102
|
-
const rendered = render(
|
|
376
|
+
const rendered = render(
|
|
377
|
+
<ResponseCell response={response} status="INFO" />,
|
|
378
|
+
{
|
|
379
|
+
messages: changeMessages,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
103
382
|
expect(
|
|
104
383
|
rendered.getByText(/uploadJob.parser.info.deprecated_implementation/i),
|
|
105
384
|
).toBeInTheDocument();
|
|
@@ -306,23 +585,38 @@ describe("<ResponseCell />", () => {
|
|
|
306
585
|
},
|
|
307
586
|
};
|
|
308
587
|
const user = userEvent.setup({ delay: null });
|
|
309
|
-
const rendered = render(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
588
|
+
const rendered = render(
|
|
589
|
+
<ResponseCell response={response} status="INFO" />,
|
|
590
|
+
{
|
|
591
|
+
messages: changeMessages,
|
|
592
|
+
},
|
|
593
|
+
);
|
|
313
594
|
expect(
|
|
314
|
-
rendered.
|
|
595
|
+
rendered.getByRole("button", { name: /2 modified fields/i }),
|
|
315
596
|
).toBeInTheDocument();
|
|
316
|
-
expect(rendered.getByText(/\(2\)/)).toBeInTheDocument();
|
|
317
597
|
await user.click(
|
|
318
|
-
rendered
|
|
319
|
-
.getByText(/uploadJob.parser.result.prop.changes/i)
|
|
320
|
-
.closest("div"),
|
|
598
|
+
rendered.getByRole("button", { name: /2 modified fields/i }),
|
|
321
599
|
);
|
|
322
600
|
expect(rendered.getByText(/New Name/i)).toBeInTheDocument();
|
|
323
601
|
expect(rendered.getByText(/Updated description/i)).toBeInTheDocument();
|
|
324
602
|
});
|
|
325
603
|
|
|
604
|
+
it("does not render changes for created INFO events", () => {
|
|
605
|
+
const rendered = render(
|
|
606
|
+
<ResultDetailCell
|
|
607
|
+
response={{
|
|
608
|
+
type: "created",
|
|
609
|
+
details: { changes: { name: "Created name" } },
|
|
610
|
+
}}
|
|
611
|
+
status="INFO"
|
|
612
|
+
/>,
|
|
613
|
+
{ messages: changeMessages },
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
expect(rendered.queryByRole("button")).not.toBeInTheDocument();
|
|
617
|
+
expect(rendered.queryByRole("table")).not.toBeInTheDocument();
|
|
618
|
+
});
|
|
619
|
+
|
|
326
620
|
it("renders INFO status with nested df_content in changes", async () => {
|
|
327
621
|
const response = {
|
|
328
622
|
type: "implementation_updated",
|
|
@@ -335,14 +629,118 @@ describe("<ResponseCell />", () => {
|
|
|
335
629
|
},
|
|
336
630
|
};
|
|
337
631
|
const user = userEvent.setup({ delay: null });
|
|
338
|
-
const rendered = render(
|
|
339
|
-
|
|
340
|
-
|
|
632
|
+
const rendered = render(
|
|
633
|
+
<ResponseCell response={response} status="INFO" />,
|
|
634
|
+
{
|
|
635
|
+
messages: changeMessages,
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
await user.click(
|
|
639
|
+
rendered.getByRole("button", { name: /1 modified field/i }),
|
|
640
|
+
);
|
|
641
|
+
expect(rendered.getByText(/inner_key/i)).toBeInTheDocument();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("renders change_details as a translated old/new values table", async () => {
|
|
645
|
+
const response = {
|
|
646
|
+
type: "updated",
|
|
647
|
+
details: {
|
|
648
|
+
change_details: {
|
|
649
|
+
goal: { old: 80, new: 90 },
|
|
650
|
+
df_content: {
|
|
651
|
+
frequency: {
|
|
652
|
+
old: {
|
|
653
|
+
value: "a very long previous value that must remain accessible",
|
|
654
|
+
origin: "user",
|
|
655
|
+
},
|
|
656
|
+
new: {
|
|
657
|
+
value: "a very long new value that must remain accessible",
|
|
658
|
+
origin: "file",
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
const user = userEvent.setup({ delay: null });
|
|
666
|
+
const rendered = render(
|
|
667
|
+
<ResponseCell response={response} status="INFO" />,
|
|
668
|
+
{
|
|
669
|
+
messages: changeMessages,
|
|
670
|
+
},
|
|
341
671
|
);
|
|
342
|
-
|
|
672
|
+
|
|
673
|
+
const toggle = rendered.getByRole("button", {
|
|
674
|
+
name: /2 modified fields/i,
|
|
675
|
+
});
|
|
676
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
677
|
+
expect(toggle).toHaveAttribute("aria-controls");
|
|
678
|
+
await user.click(toggle);
|
|
679
|
+
|
|
680
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
681
|
+
expect(rendered.getByText("Field")).toBeInTheDocument();
|
|
682
|
+
expect(rendered.getByText("Previous value")).toBeInTheDocument();
|
|
683
|
+
expect(rendered.getByText("New value")).toBeInTheDocument();
|
|
684
|
+
expect(rendered.getByText("80")).toBeInTheDocument();
|
|
685
|
+
expect(rendered.getByText("90")).toBeInTheDocument();
|
|
343
686
|
expect(
|
|
344
|
-
rendered.getByText(
|
|
345
|
-
|
|
687
|
+
rendered.getByText(
|
|
688
|
+
"a very long previous value that must remain accessible",
|
|
689
|
+
),
|
|
690
|
+
).toHaveAttribute(
|
|
691
|
+
"title",
|
|
692
|
+
"a very long previous value that must remain accessible",
|
|
693
|
+
);
|
|
694
|
+
expect(
|
|
695
|
+
rendered.getByText("a very long new value that must remain accessible"),
|
|
696
|
+
).toHaveAttribute(
|
|
697
|
+
"title",
|
|
698
|
+
"a very long new value that must remain accessible",
|
|
699
|
+
);
|
|
700
|
+
const arrows = rendered.getAllByText("→");
|
|
701
|
+
expect(arrows).toHaveLength(2);
|
|
702
|
+
arrows.forEach((arrow) => {
|
|
703
|
+
expect(arrow).toHaveAttribute("aria-hidden", "true");
|
|
704
|
+
expect(arrow.closest("span")).toHaveAttribute("aria-hidden", "true");
|
|
705
|
+
expect(arrow.closest("td")).not.toHaveAttribute("aria-hidden");
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("renders boolean, null and zero change values without losing their values", async () => {
|
|
710
|
+
const user = userEvent.setup({ delay: null });
|
|
711
|
+
const rendered = render(
|
|
712
|
+
<ResponseCell
|
|
713
|
+
response={{
|
|
714
|
+
type: "updated",
|
|
715
|
+
details: {
|
|
716
|
+
change_details: {
|
|
717
|
+
enabled: { old: false, new: true },
|
|
718
|
+
nullable: { old: null, new: null },
|
|
719
|
+
count: { old: 0, new: 0 },
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
}}
|
|
723
|
+
status="INFO"
|
|
724
|
+
/>,
|
|
725
|
+
{ messages: changeMessages },
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
await user.click(
|
|
729
|
+
rendered.getByRole("button", { name: /3 modified fields/i }),
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
const falseValue = rendered.getByText("false");
|
|
733
|
+
const trueValue = rendered.getByText("true");
|
|
734
|
+
const emptyValues = rendered.getAllByText("—");
|
|
735
|
+
const zeroValues = rendered.getAllByText("0");
|
|
736
|
+
|
|
737
|
+
expect(falseValue).toHaveAttribute("title", "false");
|
|
738
|
+
expect(trueValue).toHaveAttribute("title", "true");
|
|
739
|
+
expect(emptyValues).toHaveLength(2);
|
|
740
|
+
expect(emptyValues[0]).toHaveAttribute("title", "—");
|
|
741
|
+
expect(zeroValues).toHaveLength(2);
|
|
742
|
+
expect(zeroValues[0]).toHaveAttribute("title", "0");
|
|
743
|
+
expect(falseValue).not.toHaveAttribute("aria-label");
|
|
346
744
|
});
|
|
347
745
|
|
|
348
746
|
it("renders ERROR with sheet and row_number in response", () => {
|
|
@@ -372,3 +770,139 @@ describe("<ResponseCell />", () => {
|
|
|
372
770
|
expect(rendered.container.firstChild).toBeNull();
|
|
373
771
|
});
|
|
374
772
|
});
|
|
773
|
+
|
|
774
|
+
describe("<ResultDetailCell /> errors", () => {
|
|
775
|
+
const response = {
|
|
776
|
+
type: "field_validation_error",
|
|
777
|
+
sheet: "Sheet1",
|
|
778
|
+
row_number: 5,
|
|
779
|
+
details: {
|
|
780
|
+
errors: [
|
|
781
|
+
{ field: "name", message: "Required" },
|
|
782
|
+
{ field: "external_id", message: "Already exists" },
|
|
783
|
+
],
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
it("renders structured errors collapsed and expands them as a Field/Error table", async () => {
|
|
788
|
+
const user = userEvent.setup({ delay: null });
|
|
789
|
+
const rendered = render(
|
|
790
|
+
<ResultDetailCell response={response} status="ERROR" />,
|
|
791
|
+
{ messages: changeMessages },
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
const toggle = rendered.getByRole("button", {
|
|
795
|
+
name: "2 fields with errors",
|
|
796
|
+
});
|
|
797
|
+
expect(toggle).not.toHaveAccessibleName(/field validation error/i);
|
|
798
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
799
|
+
expect(toggle).toHaveAttribute("aria-controls");
|
|
800
|
+
expect(rendered.queryByText("Required")).not.toBeInTheDocument();
|
|
801
|
+
expect(rendered.queryByText("Sheet1")).not.toBeInTheDocument();
|
|
802
|
+
|
|
803
|
+
await user.click(toggle);
|
|
804
|
+
|
|
805
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
806
|
+
expect(
|
|
807
|
+
rendered.getByRole("columnheader", { name: "Field" }),
|
|
808
|
+
).toBeInTheDocument();
|
|
809
|
+
expect(
|
|
810
|
+
rendered.getByRole("columnheader", { name: "Error" }),
|
|
811
|
+
).toBeInTheDocument();
|
|
812
|
+
expect(
|
|
813
|
+
rendered.getByRole("rowheader", { name: "name" }),
|
|
814
|
+
).toBeInTheDocument();
|
|
815
|
+
expect(
|
|
816
|
+
rendered.getByRole("rowheader", { name: "external_id" }),
|
|
817
|
+
).toBeInTheDocument();
|
|
818
|
+
expect(rendered.getByText("Required")).toBeInTheDocument();
|
|
819
|
+
expect(rendered.getByText("Already exists")).toBeInTheDocument();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("does not render an empty disclosure when the error has no details", () => {
|
|
823
|
+
const rendered = render(
|
|
824
|
+
<ResultDetailCell
|
|
825
|
+
response={{ type: "field_validation_error" }}
|
|
826
|
+
status="ERROR"
|
|
827
|
+
/>,
|
|
828
|
+
{ messages: changeMessages },
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
expect(rendered.queryByRole("button")).not.toBeInTheDocument();
|
|
832
|
+
expect(rendered.getByText("Field validation error")).toBeInTheDocument();
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("renders data_structure_not_found as a simple message without technical details", () => {
|
|
836
|
+
const rendered = render(
|
|
837
|
+
<ResultDetailCell
|
|
838
|
+
response={{
|
|
839
|
+
type: "data_structure_not_found",
|
|
840
|
+
details: { external_id: "unknown-note", data_structure_id: 123 },
|
|
841
|
+
}}
|
|
842
|
+
status="ERROR"
|
|
843
|
+
/>,
|
|
844
|
+
{ messages: changeMessages },
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
expect(rendered.getByText("Data structure not found")).toBeInTheDocument();
|
|
848
|
+
expect(rendered.queryByRole("button")).not.toBeInTheDocument();
|
|
849
|
+
expect(rendered.queryByRole("table")).not.toBeInTheDocument();
|
|
850
|
+
expect(rendered.queryByText("unknown-note")).not.toBeInTheDocument();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("uses the singular error count for one field", () => {
|
|
854
|
+
const rendered = render(
|
|
855
|
+
<ResultDetailCell
|
|
856
|
+
response={{
|
|
857
|
+
type: "field_validation_error",
|
|
858
|
+
details: { errors: [{ field: "name", message: "Required" }] },
|
|
859
|
+
}}
|
|
860
|
+
status="ERROR"
|
|
861
|
+
/>,
|
|
862
|
+
{ messages: changeMessages },
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
expect(
|
|
866
|
+
rendered.getByRole("button", {
|
|
867
|
+
name: "1 field with an error",
|
|
868
|
+
}),
|
|
869
|
+
).toBeInTheDocument();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("renders notes change_details with previous and new values", async () => {
|
|
873
|
+
const user = userEvent.setup({ delay: null });
|
|
874
|
+
const rendered = render(
|
|
875
|
+
<ResultDetailCell
|
|
876
|
+
response={{
|
|
877
|
+
type: "updated",
|
|
878
|
+
details: {
|
|
879
|
+
external_id: "EXT-001",
|
|
880
|
+
data_structure_id: 123,
|
|
881
|
+
changes: { status: "published" },
|
|
882
|
+
change_details: {
|
|
883
|
+
status: { old: "draft", new: "published" },
|
|
884
|
+
df_content: {
|
|
885
|
+
field: {
|
|
886
|
+
old: { value: "old_value", origin: "user" },
|
|
887
|
+
new: { value: "new_value", origin: "file" },
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
}}
|
|
893
|
+
status="INFO"
|
|
894
|
+
scope="notes"
|
|
895
|
+
/>,
|
|
896
|
+
{ messages: changeMessages },
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
await user.click(
|
|
900
|
+
rendered.getByRole("button", { name: /2 modified fields/i }),
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
expect(rendered.getByText("draft")).toBeInTheDocument();
|
|
904
|
+
expect(rendered.getByText("published")).toBeInTheDocument();
|
|
905
|
+
expect(rendered.getByText("old_value")).toBeInTheDocument();
|
|
906
|
+
expect(rendered.getByText("new_value")).toBeInTheDocument();
|
|
907
|
+
});
|
|
908
|
+
});
|