@thepalaceproject/circulation-admin 1.43.0-post.14 → 1.43.0-post.15
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 +1 -1
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/EditableConfigList.test.tsx +598 -7
- package/tests/jest/components/ProtocolFormField.test.tsx +443 -7
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
|
@@ -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
|
+
});
|