@thepalaceproject/circulation-admin 1.43.0-post.9 → 1.44.0-post.3

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.
Files changed (73) hide show
  1. package/CLAUDE.md +22 -10
  2. package/README.md +11 -5
  3. package/dist/circulation-admin.js +1 -1
  4. package/jest.config.js +11 -44
  5. package/package.json +1 -5
  6. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -0
  7. package/tests/jest/components/Announcement.test.tsx +76 -0
  8. package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
  9. package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
  10. package/tests/jest/components/Autocomplete.test.tsx +67 -0
  11. package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
  12. package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
  13. package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
  14. package/tests/jest/components/BookEditForm.test.tsx +352 -0
  15. package/tests/jest/components/BookEditor.test.tsx +20 -21
  16. package/tests/jest/components/CatalogPage.test.tsx +158 -0
  17. package/tests/jest/components/CatalogServices.test.tsx +38 -1
  18. package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
  19. package/tests/jest/components/Classifications.test.tsx +166 -0
  20. package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
  21. package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
  22. package/tests/jest/components/Collections.test.tsx +335 -7
  23. package/tests/jest/components/ColorPicker.test.tsx +58 -0
  24. package/tests/jest/components/ComplaintForm.test.tsx +171 -0
  25. package/tests/jest/components/Complaints.test.tsx +259 -0
  26. package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
  27. package/tests/jest/components/ContextProvider.test.tsx +138 -0
  28. package/tests/jest/components/Contributors.test.tsx +157 -0
  29. package/tests/jest/components/CustomListEditor.test.tsx +367 -0
  30. package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
  31. package/tests/jest/components/CustomLists.test.tsx +621 -4
  32. package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
  33. package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
  34. package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
  35. package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
  36. package/tests/jest/components/EditableConfigList.test.tsx +598 -7
  37. package/tests/jest/components/EditableInput.test.tsx +250 -1
  38. package/tests/jest/components/EditorField.test.tsx +95 -0
  39. package/tests/jest/components/GenreForm.test.tsx +152 -0
  40. package/tests/jest/components/Header.test.tsx +421 -0
  41. package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
  42. package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
  43. package/tests/jest/components/InputList.test.tsx +493 -0
  44. package/tests/jest/components/Lane.test.tsx +132 -10
  45. package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
  46. package/tests/jest/components/LaneEditor.test.tsx +377 -9
  47. package/tests/jest/components/Lanes.test.tsx +446 -0
  48. package/tests/jest/components/LanesSidebar.test.tsx +621 -0
  49. package/tests/jest/components/Libraries.test.tsx +48 -0
  50. package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
  51. package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
  52. package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
  53. package/tests/jest/components/MetadataServices.test.tsx +37 -1
  54. package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
  55. package/tests/jest/components/PairedMenus.test.tsx +220 -0
  56. package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
  57. package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
  58. package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
  59. package/tests/jest/components/SelfTests.test.tsx +601 -0
  60. package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
  61. package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
  62. package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
  63. package/tests/jest/components/Stats.test.tsx +0 -7
  64. package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
  65. package/tests/jest/components/ToolTip.test.tsx +46 -0
  66. package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
  67. package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
  68. package/tests/jest/features/book.test.ts +75 -80
  69. package/tests/jest/fixtures/classificationsData.ts +45 -0
  70. package/tests/jest/fixtures/genreData.ts +811 -0
  71. package/tests/jest/README.md +0 -6
  72. package/tests/jest/legacy-setup.ts +0 -13
  73. package/tests/jest/legacyJsdomEnvironment.js +0 -49
@@ -0,0 +1,601 @@
1
+ import * as React from "react";
2
+ import {
3
+ screen,
4
+ waitFor,
5
+ waitForElementToBeRemoved,
6
+ within,
7
+ } from "@testing-library/react";
8
+ import userEvent from "@testing-library/user-event";
9
+
10
+ import { renderWithProviders } from "../testUtils/withProviders";
11
+ import buildStore from "../../../src/store";
12
+ // The default export is the connected component. Rendering it (rather than the
13
+ // unconnected `SelfTests` class) exercises `mapStateToProps` /
14
+ // `mapDispatchToProps`, whose wiring nothing else covers.
15
+ import SelfTests from "../../../src/components/SelfTests";
16
+
17
+ // SelfTests can take more than just a collection (an integration can have self
18
+ // tests), but we only exercise collection data here.
19
+ const collections: any[] = [
20
+ {
21
+ id: 1,
22
+ name: "collection 1",
23
+ protocol: "protocol",
24
+ libraries: [{ short_name: "library" }],
25
+ settings: {
26
+ external_account_id: "nypl",
27
+ },
28
+ self_test_results: {
29
+ duration: 1.75,
30
+ start: "2018-08-07T19:34:54Z",
31
+ end: "2018-08-07T19:34:55Z",
32
+ results: [
33
+ {
34
+ duration: 0.000119,
35
+ end: "2018-08-07T19:34:54Z",
36
+ exception: null,
37
+ name: "Initial setup.",
38
+ result: null,
39
+ start: "2018-08-07T19:34:54Z",
40
+ success: true,
41
+ },
42
+ ],
43
+ },
44
+ },
45
+ {
46
+ id: 1,
47
+ name: "collection 1",
48
+ protocol: "protocol",
49
+ libraries: [{ short_name: "library" }],
50
+ settings: {
51
+ external_account_id: "nypl",
52
+ },
53
+ self_test_results: {
54
+ duration: 1.75,
55
+ start: "2018-08-07T19:34:54Z",
56
+ end: "2018-08-07T19:34:55Z",
57
+ results: [
58
+ {
59
+ duration: 0.000119,
60
+ end: "2018-08-07T19:34:54Z",
61
+ exception: null,
62
+ name: "Initial setup.",
63
+ result: null,
64
+ start: "2018-08-07T19:34:54Z",
65
+ success: true,
66
+ },
67
+ {
68
+ duration: 0,
69
+ end: "2018-08-07T19:34:55Z",
70
+ exception: {
71
+ class: "IntegrationException",
72
+ debug_message:
73
+ "Add the collection to a library that has a patron authentication service.",
74
+ message: "Collection is not associated with any libraries.",
75
+ },
76
+ name: "Acquiring test patron credentials.",
77
+ result: null,
78
+ start: "2018-08-07T19:34:55Z",
79
+ success: false,
80
+ },
81
+ ],
82
+ },
83
+ },
84
+ {
85
+ id: 1,
86
+ name: "collection 1",
87
+ protocol: "protocol",
88
+ libraries: [{ short_name: "library" }],
89
+ settings: {
90
+ external_account_id: "nypl",
91
+ },
92
+ self_test_results: {
93
+ exception: "Exception getting self-test results for collection ...",
94
+ duration: 0,
95
+ start: "",
96
+ end: "",
97
+ results: [],
98
+ },
99
+ },
100
+ ];
101
+
102
+ const updatedCollection = {
103
+ id: 1,
104
+ name: "collection 1",
105
+ protocol: "protocol",
106
+ libraries: [{ short_name: "library" }],
107
+ settings: {
108
+ external_account_id: "nypl",
109
+ },
110
+ self_test_results: {
111
+ duration: 1.75,
112
+ start: "2018-08-07T20:34:54Z",
113
+ end: "2018-08-07T20:34:55Z",
114
+ results: [
115
+ {
116
+ duration: 0.000119,
117
+ end: "2018-08-07T20:34:54Z",
118
+ exception: null,
119
+ name: "Initial setup.",
120
+ result: null,
121
+ start: "2018-08-07T20:34:54Z",
122
+ success: true,
123
+ },
124
+ ],
125
+ },
126
+ };
127
+
128
+ // Match the component's own date formatting (local time, matching `formatDate`).
129
+ const getExpectedDate = (date: Date): string => date.toDateString();
130
+ const getExpectedTime = (date: Date): string => {
131
+ const hours = date.getHours().toString().padStart(2, "0");
132
+ const minutes = date.getMinutes().toString().padStart(2, "0");
133
+ const seconds = date.getSeconds().toString().padStart(2, "0");
134
+ return `${hours}:${minutes}:${seconds}`;
135
+ };
136
+
137
+ // The `GET_SELF_TESTS` response body. Its `self_test_results.id` never matches a
138
+ // seeded collection/patron-auth id, so the shared `hasSelfTests` load reducer
139
+ // leaves the seeded data intact (and `mapStateToProps` keeps passing
140
+ // `ownProps.item` through, since the body carries no matching `goal`).
141
+ const getSelfTestsResponse = () =>
142
+ new Response(JSON.stringify({ self_test_results: { id: 0 } }), {
143
+ status: 200,
144
+ headers: { "Content-Type": "application/json" },
145
+ });
146
+
147
+ describe("SelfTests", () => {
148
+ let fetchSpy: jest.SpyInstance;
149
+
150
+ // The connected component fetches self-test results on mount and posts to run
151
+ // them. Stub `fetch` so nothing hits the network and no promise rejects after
152
+ // teardown (which would crash the worker).
153
+ beforeEach(() => {
154
+ fetchSpy = jest.spyOn(globalThis, "fetch").mockImplementation(((
155
+ _url: string,
156
+ options: any
157
+ ) => {
158
+ if (options && options.method === "POST") {
159
+ return Promise.resolve(new Response("", { status: 200 }));
160
+ }
161
+ return Promise.resolve(getSelfTestsResponse());
162
+ }) as any);
163
+ });
164
+
165
+ afterEach(() => {
166
+ jest.restoreAllMocks();
167
+ });
168
+
169
+ // Each test gets a fresh store so the on-mount fetch does not leak state.
170
+ const renderSelfTests = (
171
+ props: Record<string, unknown>
172
+ ): { store: ReturnType<typeof buildStore> } & ReturnType<
173
+ typeof renderWithProviders
174
+ > => {
175
+ const store = buildStore();
176
+ // `GET_SELF_TESTS_LOAD` is also handled by the `collections` and
177
+ // `patronAuthServices` reducers (see `hasSelfTests`), which read `state.data`
178
+ // and throw when it is null. Seed both with a non-empty list so the on-mount
179
+ // fetch (and any re-fetch) resolves cleanly instead of rejecting and crashing
180
+ // the worker.
181
+ store.dispatch({
182
+ type: "COLLECTIONS_LOAD",
183
+ data: { collections: [{ id: 1 }] },
184
+ });
185
+ store.dispatch({
186
+ type: "PATRON_AUTH_SERVICES_LOAD",
187
+ data: { patron_auth_services: [{ id: 1 }] },
188
+ });
189
+ const utils = renderWithProviders(
190
+ <SelfTests
191
+ store={store}
192
+ type="collection"
193
+ csrfToken="token"
194
+ sortByCollection={false}
195
+ {...props}
196
+ />,
197
+ { reduxProviderProps: { store } }
198
+ );
199
+ return { store, ...utils };
200
+ };
201
+
202
+ // The on-mount `getSelfTests` GET resolves and writes `responseBody`; waiting
203
+ // for it settles the fetch before the test ends.
204
+ const settle = (store: ReturnType<typeof buildStore>) =>
205
+ waitFor(() =>
206
+ expect(store.getState().editor.selfTests.responseBody).not.toBeNull()
207
+ );
208
+
209
+ it("renders with empty self_test_results", async () => {
210
+ const { store, container } = renderSelfTests({ item: {} });
211
+ await settle(store);
212
+
213
+ expect(
214
+ container.querySelector(".integration-selftests")
215
+ ).toBeInTheDocument();
216
+ expect(container.querySelector("ul")).toBeNull();
217
+ expect(screen.getByText("No self test results found.")).toBeInTheDocument();
218
+ });
219
+
220
+ it("disables the Run tests button when the tests cannot be run", async () => {
221
+ const item = {
222
+ ...collections[0],
223
+ self_test_results: {
224
+ ...collections[0].self_test_results,
225
+ exception: "Exception!",
226
+ disabled: true,
227
+ },
228
+ };
229
+ const { store } = renderSelfTests({ item });
230
+ await settle(store);
231
+
232
+ expect(screen.getByRole("button", { name: "Run tests" })).toBeDisabled();
233
+ });
234
+
235
+ it("renders the first-time message for new services", async () => {
236
+ const exception = "This integration has no attribute 'prior_test_results'";
237
+ const item = {
238
+ ...collections[0],
239
+ self_test_results: { ...collections[0].self_test_results, exception },
240
+ };
241
+ const { store, container } = renderSelfTests({ item });
242
+ await settle(store);
243
+
244
+ expect(
245
+ container.querySelector(".integration-selftests")
246
+ ).toBeInTheDocument();
247
+ expect(container.querySelector("ul")).toBeNull();
248
+ expect(
249
+ screen.getByText("There are no self test results yet.")
250
+ ).toBeInTheDocument();
251
+ });
252
+
253
+ it("renders a results list when there are results", async () => {
254
+ const { store, container } = renderSelfTests({ item: collections[0] });
255
+ await settle(store);
256
+
257
+ expect(
258
+ container.querySelector(".integration-selftests")
259
+ ).toBeInTheDocument();
260
+ expect(container.querySelector("ul")).toBeInTheDocument();
261
+ });
262
+
263
+ it("formats the date and duration of the most recent tests", async () => {
264
+ const { store, container } = renderSelfTests({ item: collections[0] });
265
+ await settle(store);
266
+
267
+ const date = new Date(collections[0].self_test_results.start);
268
+ expect(container.querySelector(".description")).toHaveTextContent(
269
+ `Tests last ran on ${getExpectedDate(date)} ${getExpectedTime(
270
+ date
271
+ )} and lasted 1.75s.`
272
+ );
273
+ });
274
+
275
+ it("swaps in freshly fetched results when they match the item (mapStateToProps)", async () => {
276
+ // A fetched result for the same item, with a matching goal and a newer start
277
+ // time, replaces the item in `mapStateToProps` (the connect wiring nothing
278
+ // else covers).
279
+ const item = {
280
+ id: 42,
281
+ name: "collection 42",
282
+ protocol: "protocol",
283
+ self_test_results: {
284
+ duration: 1,
285
+ start: "2018-08-07T10:00:00Z",
286
+ end: "2018-08-07T10:00:01Z",
287
+ results: [
288
+ { name: "old result", success: true, result: null, exception: null },
289
+ ],
290
+ },
291
+ };
292
+ fetchSpy.mockImplementation(((_url: string, options: any) => {
293
+ if (options && options.method === "POST") {
294
+ return Promise.resolve(new Response("", { status: 200 }));
295
+ }
296
+ return Promise.resolve(
297
+ new Response(
298
+ JSON.stringify({
299
+ self_test_results: {
300
+ goal: "collection",
301
+ id: 42,
302
+ self_test_results: {
303
+ duration: 2.5,
304
+ start: "2018-08-07T23:00:00Z",
305
+ end: "2018-08-07T23:00:01Z",
306
+ results: [
307
+ {
308
+ name: "new result",
309
+ success: true,
310
+ result: null,
311
+ exception: null,
312
+ },
313
+ ],
314
+ },
315
+ },
316
+ }),
317
+ { status: 200, headers: { "Content-Type": "application/json" } }
318
+ )
319
+ );
320
+ }) as any);
321
+
322
+ const { container } = renderSelfTests({ item });
323
+
324
+ // The fetched results supersede the originally-passed item.
325
+ await waitFor(() =>
326
+ expect(container.querySelector(".description")).toHaveTextContent(
327
+ "and lasted 2.50s."
328
+ )
329
+ );
330
+ expect(
331
+ screen.getByRole("heading", { name: "new result" })
332
+ ).toBeInTheDocument();
333
+ });
334
+
335
+ it("adopts a newer result but keeps the most recent one when an older result arrives", async () => {
336
+ const { store, container, rerender } = renderSelfTests({
337
+ item: collections[0],
338
+ });
339
+ await settle(store);
340
+
341
+ const initialDate = new Date(collections[0].self_test_results.start);
342
+ expect(container.querySelector(".description")).toHaveTextContent(
343
+ getExpectedTime(initialDate)
344
+ );
345
+
346
+ // The new item has a more recent start time, so it becomes the most recent.
347
+ rerender(
348
+ <SelfTests
349
+ store={store}
350
+ type="collection"
351
+ csrfToken="token"
352
+ sortByCollection={false}
353
+ item={updatedCollection}
354
+ />
355
+ );
356
+ const newerDate = new Date(updatedCollection.self_test_results.start);
357
+ expect(container.querySelector(".description")).toHaveTextContent(
358
+ getExpectedTime(newerDate)
359
+ );
360
+
361
+ // This item is older, so the most recent result is unchanged.
362
+ rerender(
363
+ <SelfTests
364
+ store={store}
365
+ type="collection"
366
+ csrfToken="token"
367
+ sortByCollection={false}
368
+ item={collections[1]}
369
+ />
370
+ );
371
+ expect(container.querySelector(".description")).toHaveTextContent(
372
+ getExpectedTime(newerDate)
373
+ );
374
+ });
375
+
376
+ describe("Successful self tests", () => {
377
+ it("shows a success icon and the summary for a passing collection", async () => {
378
+ const { store, container } = renderSelfTests({ item: collections[0] });
379
+ await settle(store);
380
+
381
+ // There's only one self test result in the collection and it passes.
382
+ expect(container.querySelector("svg.success")).toBeInTheDocument();
383
+ expect(container.querySelector("svg.failure")).toBeNull();
384
+
385
+ const date = new Date(collections[0].self_test_results.start);
386
+ expect(container.querySelector(".description")).toHaveTextContent(
387
+ `Tests last ran on ${getExpectedDate(date)} ${getExpectedTime(
388
+ date
389
+ )} and lasted 1.75s.`
390
+ );
391
+ });
392
+
393
+ it("shows detail for each passing result", async () => {
394
+ const { store, container } = renderSelfTests({ item: collections[0] });
395
+ await settle(store);
396
+
397
+ const items = container.querySelectorAll("ul li");
398
+ expect(items).toHaveLength(1);
399
+ expect(items[0]).toHaveClass("success");
400
+ expect(
401
+ within(items[0] as HTMLElement).getByRole("heading", {
402
+ name: "Initial setup.",
403
+ })
404
+ ).toBeInTheDocument();
405
+ expect(items[0].querySelector(".success-description")).toHaveTextContent(
406
+ "success: true"
407
+ );
408
+ });
409
+ });
410
+
411
+ describe("Unsuccessful self tests", () => {
412
+ it("shows the base error message when results could not be retrieved", async () => {
413
+ const { store, container } = renderSelfTests({ item: collections[2] });
414
+ await settle(store);
415
+
416
+ expect(container.querySelector(".description")).toHaveTextContent(
417
+ "Exception getting self-test results for collection ..."
418
+ );
419
+ });
420
+
421
+ it("shows a failure icon and summary when a test failed", async () => {
422
+ const { store, container } = renderSelfTests({ item: collections[1] });
423
+ await settle(store);
424
+
425
+ // Two self tests ran but one failed, so a failing icon is shown.
426
+ expect(container.querySelector("svg.failure")).toBeInTheDocument();
427
+ expect(container.querySelector("svg.success")).toBeNull();
428
+
429
+ const date = new Date(collections[1].self_test_results.start);
430
+ expect(container.querySelector(".description")).toHaveTextContent(
431
+ `Tests last ran on ${getExpectedDate(date)} ${getExpectedTime(
432
+ date
433
+ )} and lasted 1.75s.`
434
+ );
435
+ });
436
+
437
+ it("shows detail for a mix of passing and failing results", async () => {
438
+ const { store, container } = renderSelfTests({ item: collections[1] });
439
+ await settle(store);
440
+
441
+ const items = container.querySelectorAll("ul li");
442
+ expect(items).toHaveLength(2);
443
+
444
+ expect(items[0]).toHaveClass("success");
445
+ expect(
446
+ within(items[0] as HTMLElement).getByRole("heading", {
447
+ name: "Initial setup.",
448
+ })
449
+ ).toBeInTheDocument();
450
+ expect(items[0].querySelector(".success-description")).toHaveTextContent(
451
+ "success: true"
452
+ );
453
+ expect(items[0].querySelector(".exception-description")).toBeNull();
454
+
455
+ expect(items[1]).toHaveClass("failure");
456
+ expect(
457
+ within(items[1] as HTMLElement).getByRole("heading", {
458
+ name: "Acquiring test patron credentials.",
459
+ })
460
+ ).toBeInTheDocument();
461
+ expect(items[1].querySelector(".success-description")).toHaveTextContent(
462
+ "success: false"
463
+ );
464
+ expect(
465
+ items[1].querySelector(".exception-description")
466
+ ).toHaveTextContent(
467
+ "exception: Collection is not associated with any libraries."
468
+ );
469
+ });
470
+ });
471
+
472
+ describe("Get new results", () => {
473
+ it("runs the self tests and shows the running indicator while in flight", async () => {
474
+ let resolveRun: () => void;
475
+ fetchSpy.mockImplementation(((_url: string, options: any) => {
476
+ if (options && options.method === "POST") {
477
+ return new Promise((resolve) => {
478
+ resolveRun = () => resolve(new Response("", { status: 200 }));
479
+ });
480
+ }
481
+ return Promise.resolve(getSelfTestsResponse());
482
+ }) as any);
483
+
484
+ const { store } = renderSelfTests({ item: collections[0] });
485
+ await settle(store);
486
+
487
+ const user = userEvent.setup();
488
+ await user.click(screen.getByRole("button", { name: "Run tests" }));
489
+
490
+ // Dispatching runSelfTests flips the store to fetching, so the component
491
+ // shows the running indicator and posts to the self-tests endpoint.
492
+ expect(
493
+ await screen.findByText("Running new self tests")
494
+ ).toBeInTheDocument();
495
+ expect(fetchSpy).toHaveBeenCalledWith(
496
+ "/admin/collection_self_tests/1",
497
+ expect.objectContaining({ method: "POST" })
498
+ );
499
+
500
+ // Resolving the request clears the indicator.
501
+ resolveRun();
502
+ await waitForElementToBeRemoved(() =>
503
+ screen.queryByText("Running new self tests")
504
+ );
505
+ });
506
+
507
+ it("shows an error alert when running the self tests fails", async () => {
508
+ fetchSpy.mockImplementation(((_url: string, options: any) => {
509
+ if (options && options.method === "POST") {
510
+ return Promise.resolve(
511
+ new Response(
512
+ JSON.stringify({ detail: "Failed to run new tests." }),
513
+ {
514
+ status: 400,
515
+ headers: { "Content-Type": "application/json" },
516
+ }
517
+ )
518
+ );
519
+ }
520
+ return Promise.resolve(getSelfTestsResponse());
521
+ }) as any);
522
+
523
+ const { store, container } = renderSelfTests({ item: collections[0] });
524
+ await settle(store);
525
+ expect(container.querySelector(".alert")).toBeNull();
526
+
527
+ const user = userEvent.setup();
528
+ await user.click(screen.getByRole("button", { name: "Run tests" }));
529
+
530
+ expect(
531
+ await screen.findByText("Error: Failed to run new tests.")
532
+ ).toBeInTheDocument();
533
+ expect(container.querySelector(".alert-danger")).toBeInTheDocument();
534
+ });
535
+ });
536
+
537
+ describe("Metadata (sortByCollection) results", () => {
538
+ const base = collections[0].self_test_results.results[0];
539
+ const metadataItem = {
540
+ ...collections[0],
541
+ self_test_results: {
542
+ ...collections[0].self_test_results,
543
+ results: [
544
+ { ...base, collection: "A", name: "Test 1", success: true },
545
+ { ...base, collection: "B", name: "Test 1", success: true },
546
+ { ...base, collection: "A", name: "Test 2", success: true },
547
+ { ...base, collection: "C", name: "Test 1", success: false },
548
+ ],
549
+ },
550
+ };
551
+
552
+ it("groups results into one panel per collection", async () => {
553
+ const { store, container } = renderSelfTests({
554
+ item: metadataItem,
555
+ sortByCollection: true,
556
+ });
557
+ await settle(store);
558
+
559
+ const titles = Array.from(container.querySelectorAll(".panel-title")).map(
560
+ (el) => el.textContent
561
+ );
562
+ expect(titles).toStrictEqual(["A", "B", "C"]);
563
+ });
564
+
565
+ it("marks a collection panel danger when any of its tests failed", async () => {
566
+ const { store, container } = renderSelfTests({
567
+ item: metadataItem,
568
+ sortByCollection: true,
569
+ });
570
+ await settle(store);
571
+
572
+ const panels = Array.from(container.querySelectorAll(".panel"));
573
+ const panelFor = (name: string) =>
574
+ panels.find(
575
+ (p) => p.querySelector(".panel-title")?.textContent === name
576
+ );
577
+ expect(panelFor("A")).toHaveClass("panel-success");
578
+ expect(panelFor("B")).toHaveClass("panel-success");
579
+ expect(panelFor("C")).toHaveClass("panel-danger");
580
+ });
581
+
582
+ it("labels the initial-setup collection 'Initial Setup'", async () => {
583
+ const initialItem = {
584
+ ...collections[0],
585
+ self_test_results: {
586
+ ...collections[0].self_test_results,
587
+ results: [{ ...base, collection: "undefined", name: "Test 1" }],
588
+ },
589
+ };
590
+ const { store, container } = renderSelfTests({
591
+ item: initialItem,
592
+ sortByCollection: true,
593
+ });
594
+ await settle(store);
595
+
596
+ expect(container.querySelector(".panel-title")).toHaveTextContent(
597
+ "Initial Setup"
598
+ );
599
+ });
600
+ });
601
+ });