@thepalaceproject/circulation-admin 1.43.0 → 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.
- package/CLAUDE.md +28 -7
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/eslint.config.mjs +9 -1
- package/jest.config.js +22 -8
- package/package.json +6 -20
- package/tests/jest/actions.test.ts +962 -0
- package/tests/jest/components/AccountPage.test.tsx +41 -0
- package/tests/jest/components/AdvancedSearchBooleanFilter.test.tsx +231 -0
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +93 -20
- package/tests/jest/components/AdvancedSearchFilter.test.tsx +142 -0
- package/tests/jest/components/AdvancedSearchFilterInput.test.tsx +212 -0
- package/tests/jest/components/AdvancedSearchFilterViewer.test.tsx +52 -0
- package/tests/jest/components/AdvancedSearchValueFilter.test.tsx +123 -0
- package/tests/jest/components/Announcement.test.tsx +76 -0
- package/tests/jest/components/AnnouncementForm.test.tsx +229 -0
- package/tests/jest/components/AnnouncementsSection.test.tsx +146 -0
- package/tests/jest/components/Autocomplete.test.tsx +67 -0
- package/tests/jest/components/BookCoverEditor.test.tsx +393 -0
- package/tests/jest/components/BookDetails.test.tsx +133 -0
- package/tests/jest/components/BookDetailsContainer.test.tsx +116 -0
- package/tests/jest/components/BookDetailsEditor.test.tsx +275 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/BookEditForm.test.tsx +352 -0
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/CatalogPage.test.tsx +158 -0
- package/tests/jest/components/CatalogServices.test.tsx +100 -0
- package/tests/jest/components/ChangePasswordForm.test.tsx +163 -0
- package/tests/jest/components/CirculationEventsDownloadForm.test.tsx +41 -0
- package/tests/jest/components/Classifications.test.tsx +166 -0
- package/tests/jest/components/ClassificationsForm.test.tsx +458 -0
- package/tests/jest/components/ClassificationsTable.test.tsx +57 -0
- package/tests/jest/components/Collections.test.tsx +335 -7
- package/tests/jest/components/ColorPicker.test.tsx +58 -0
- package/tests/jest/components/ComplaintForm.test.tsx +171 -0
- package/tests/jest/components/Complaints.test.tsx +259 -0
- package/tests/jest/components/ConfigPage.test.tsx +58 -0
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/ContextProvider.test.tsx +138 -0
- package/tests/jest/components/Contributors.test.tsx +157 -0
- package/tests/jest/components/CustomListEditor.test.tsx +367 -0
- package/tests/jest/components/CustomListEntriesEditor.test.tsx +1240 -0
- package/tests/jest/components/CustomListPage.test.tsx +57 -0
- package/tests/jest/components/CustomListSearch.test.tsx +275 -0
- package/tests/jest/components/CustomLists.test.tsx +621 -4
- package/tests/jest/components/CustomListsForBook.test.tsx +353 -0
- package/tests/jest/components/CustomListsSidebar.test.tsx +251 -0
- package/tests/jest/components/DashboardPage.test.tsx +76 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +1 -5
- package/tests/jest/components/DiagnosticsServiceTabs.test.tsx +141 -0
- package/tests/jest/components/DiagnosticsServiceType.test.tsx +75 -0
- package/tests/jest/components/DiagnosticsTabContainer.test.tsx +147 -0
- package/tests/jest/components/DiscoveryServices.test.tsx +154 -7
- package/tests/jest/components/EditableConfigList.test.tsx +606 -9
- package/tests/jest/components/EditableInput.test.tsx +250 -1
- package/tests/jest/components/EditorField.test.tsx +95 -0
- package/tests/jest/components/EntryPointsContainer.test.tsx +97 -0
- package/tests/jest/components/EntryPointsTabs.test.tsx +88 -0
- package/tests/jest/components/ErrorMessage.test.tsx +97 -0
- package/tests/jest/components/Footer.test.tsx +141 -0
- package/tests/jest/components/GenreForm.test.tsx +152 -0
- package/tests/jest/components/Header.test.tsx +421 -0
- package/tests/jest/components/IndividualAdminEditForm.test.tsx +506 -1
- package/tests/jest/components/IndividualAdmins.test.tsx +124 -8
- package/tests/jest/components/InputList.test.tsx +493 -0
- package/tests/jest/components/InventoryReportRequestModal.test.tsx +16 -42
- package/tests/jest/components/JsonField.test.tsx +20 -0
- package/tests/jest/components/Lane.test.tsx +132 -10
- package/tests/jest/components/LaneCustomListsEditor.test.tsx +450 -0
- package/tests/jest/components/LaneEditor.test.tsx +377 -13
- package/tests/jest/components/LanePage.test.tsx +68 -0
- package/tests/jest/components/Lanes.test.tsx +446 -0
- package/tests/jest/components/LanesSidebar.test.tsx +621 -0
- package/tests/jest/components/LanguageField.test.tsx +40 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/LibraryEditForm.test.tsx +383 -0
- package/tests/jest/components/LibraryRegistration.test.tsx +354 -0
- package/tests/jest/components/LibraryRegistrationForm.test.tsx +203 -0
- package/tests/jest/components/LoadButton.test.tsx +49 -0
- package/tests/jest/components/ManagePatrons.test.tsx +73 -0
- package/tests/jest/components/ManagePatronsForm.test.tsx +180 -0
- package/tests/jest/components/ManagePatronsTabContainer.test.tsx +143 -0
- package/tests/jest/components/MetadataServices.test.tsx +95 -0
- package/tests/jest/components/NeighborhoodAnalyticsForm.test.tsx +167 -0
- package/tests/jest/components/PairedMenus.test.tsx +220 -0
- package/tests/jest/components/PatronAuthServices.test.tsx +114 -0
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +3 -1
- package/tests/jest/components/PatronInfo.test.tsx +54 -0
- package/tests/jest/components/ProtocolFormField.test.tsx +479 -7
- package/tests/jest/components/ResetAdobeId.test.tsx +302 -0
- package/tests/jest/components/SaveButton.test.tsx +47 -0
- package/tests/jest/components/SelfTestResult.test.tsx +164 -0
- package/tests/jest/components/SelfTests.test.tsx +601 -0
- package/tests/jest/components/SelfTestsCategory.test.tsx +200 -0
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
- package/tests/jest/components/ServiceEditForm.test.tsx +1092 -0
- package/tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx +110 -0
- package/tests/jest/components/SetupPage.test.tsx +41 -0
- package/tests/jest/components/SingleStatListItem.test.tsx +62 -0
- package/tests/jest/components/SitewideAnnouncements.test.tsx +134 -0
- package/tests/jest/components/Stats.test.tsx +4 -15
- package/tests/jest/components/TabContainer.test.tsx +68 -0
- package/tests/jest/components/TextWithEditMode.test.tsx +218 -0
- package/tests/jest/components/Timestamp.test.tsx +74 -0
- package/tests/jest/components/ToolTip.test.tsx +46 -0
- package/tests/jest/components/TroubleshootingCategoryPage.test.tsx +69 -0
- package/tests/jest/components/TroubleshootingPage.test.tsx +94 -0
- package/tests/jest/components/TroubleshootingTabContainer.test.tsx +68 -0
- package/tests/jest/components/UpdatingLoader.test.tsx +36 -0
- package/tests/jest/components/WelcomePage.test.tsx +43 -0
- package/tests/jest/components/WithEditButton.test.tsx +55 -0
- package/tests/jest/components/WithRemoveButton.test.tsx +95 -0
- package/tests/jest/editorAdapter.test.ts +245 -0
- package/tests/jest/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/index.test.tsx +50 -0
- package/tests/jest/models/Admin.test.ts +206 -0
- package/tests/jest/reducers/bookCoverPreview.test.ts +81 -0
- package/tests/jest/reducers/classifications.test.ts +161 -0
- package/tests/jest/reducers/collections.test.ts +96 -0
- package/tests/jest/reducers/complaints.test.ts +166 -0
- package/tests/jest/reducers/createFetchEditReducer.test.ts +220 -0
- package/tests/jest/reducers/createRegisterLibraryReducer.test.ts +51 -0
- package/tests/jest/reducers/customListDetails.test.ts +120 -0
- package/tests/jest/reducers/customListEditor.test.ts +2823 -0
- package/tests/jest/reducers/managePatrons.test.ts +71 -0
- package/tests/jest/testUtils/formDataShim.test.ts +135 -0
- package/tests/jest/testUtils/formDataShim.ts +112 -0
- package/tests/jest/utils/sharedFunctions.test.ts +121 -0
- package/tsconfig.json +3 -4
- package/testReporter.js +0 -29
- package/tests/jest/README.md +0 -6
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
3
5
|
import { Collections } from "../../../src/components/Collections";
|
|
6
|
+
import CollectionsConnected from "../../../src/components/Collections";
|
|
4
7
|
import renderWithContext from "../testUtils/renderWithContext";
|
|
8
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
9
|
+
import buildStore from "../../../src/store";
|
|
5
10
|
import {
|
|
6
11
|
CollectionsData,
|
|
7
12
|
ConfigurationSettings,
|
|
8
13
|
} from "../../../src/interfaces";
|
|
9
14
|
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
10
15
|
|
|
11
|
-
// NB: This adds tests to the already existing tests in:
|
|
12
|
-
// - `src/components/__tests__/Collections-test.tsx`.
|
|
13
|
-
//
|
|
14
|
-
// Those tests should eventually be migrated here and
|
|
15
|
-
// adapted to the Jest/React Testing Library paradigm.
|
|
16
|
-
|
|
17
16
|
describe("Collections - associated library disclosure", () => {
|
|
18
17
|
// ── Shared fixtures ───────────────────────────────────────────────────────
|
|
19
18
|
|
|
@@ -219,3 +218,332 @@ describe("Collections - associated library disclosure", () => {
|
|
|
219
218
|
);
|
|
220
219
|
});
|
|
221
220
|
});
|
|
221
|
+
|
|
222
|
+
// These exercise behavior the disclosure tests above do not: the connect()
|
|
223
|
+
// wiring (mapStateToProps / mapDispatchToProps, which only run when the CONNECTED
|
|
224
|
+
// default export mounts), the marked-for-deletion list item, the
|
|
225
|
+
// confirm-before-disassociating-libraries behavior, the
|
|
226
|
+
// getChildContext().registerLibrary closure, the on-mount registrations fetch,
|
|
227
|
+
// and the delete flow.
|
|
228
|
+
|
|
229
|
+
describe("Collections - connected wiring, deletion, and registration", () => {
|
|
230
|
+
const allLibraries = [
|
|
231
|
+
{ short_name: "nypl", name: "NYPL", uuid: "uuid-nypl" },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const sysAdminConfig: Partial<ConfigurationSettings> = {
|
|
235
|
+
csrfToken: "",
|
|
236
|
+
featureFlags: defaultFeatureFlags,
|
|
237
|
+
roles: [{ role: "system" }],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
afterEach(() => {
|
|
241
|
+
jest.restoreAllMocks();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("connected default export", () => {
|
|
245
|
+
// A loaded `libraries` slice persists through the on-mount collections fetch
|
|
246
|
+
// (the libraries reducer ignores that action), so mapStateToProps takes its
|
|
247
|
+
// `data.allLibraries` branch on every render.
|
|
248
|
+
const loadedLibrariesState = {
|
|
249
|
+
data: { libraries: allLibraries },
|
|
250
|
+
isFetching: false,
|
|
251
|
+
isEditing: false,
|
|
252
|
+
fetchError: null,
|
|
253
|
+
formError: null,
|
|
254
|
+
isLoaded: true,
|
|
255
|
+
responseBody: null,
|
|
256
|
+
successMessage: null,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const stubFetch = (body: unknown) =>
|
|
260
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
261
|
+
async () =>
|
|
262
|
+
new Response(JSON.stringify(body), {
|
|
263
|
+
status: 200,
|
|
264
|
+
headers: { "Content-Type": "application/json" },
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
it("wires mapStateToProps/mapDispatchToProps and marks a collection for deletion", async () => {
|
|
269
|
+
const listData = {
|
|
270
|
+
collections: [
|
|
271
|
+
{ id: 2, protocol: "test protocol", name: "ODL" },
|
|
272
|
+
{
|
|
273
|
+
id: 3,
|
|
274
|
+
protocol: "test protocol",
|
|
275
|
+
name: "Enki",
|
|
276
|
+
marked_for_deletion: true,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
protocols: [{ name: "test protocol", label: "TP", settings: [] }],
|
|
280
|
+
};
|
|
281
|
+
stubFetch(listData);
|
|
282
|
+
const store = buildStore({
|
|
283
|
+
editor: { libraries: loadedLibrariesState },
|
|
284
|
+
} as any);
|
|
285
|
+
|
|
286
|
+
const { container } = renderWithProviders(
|
|
287
|
+
<CollectionsConnected csrfToken="token" />,
|
|
288
|
+
{
|
|
289
|
+
reduxProviderProps: { store },
|
|
290
|
+
appConfigSettings: sysAdminConfig,
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// The list appears once the connected component's on-mount fetch resolves
|
|
295
|
+
// and mapStateToProps feeds the fetched data back in as props.
|
|
296
|
+
expect(
|
|
297
|
+
await screen.findByRole("heading", { level: 3, name: "ODL" })
|
|
298
|
+
).toBeInTheDocument();
|
|
299
|
+
|
|
300
|
+
// The non-deleted collection has edit and delete controls.
|
|
301
|
+
const editLink = container.querySelector("a.edit-item");
|
|
302
|
+
expect(editLink.getAttribute("href")).toBe(
|
|
303
|
+
"/admin/web/config/collections/edit/2"
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// The deleted collection renders a special item with no edit/delete
|
|
307
|
+
// controls and an explanatory message.
|
|
308
|
+
const deleted = container.querySelector(".deleted-collection");
|
|
309
|
+
expect(deleted).not.toBeNull();
|
|
310
|
+
expect(deleted).toHaveTextContent("Enki");
|
|
311
|
+
expect(deleted).toHaveTextContent(/cannot be edited/);
|
|
312
|
+
expect(deleted.querySelector("a.edit-item")).toBeNull();
|
|
313
|
+
expect(deleted.querySelector("button.delete-item")).toBeNull();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("edit mode: registration and additional content", () => {
|
|
318
|
+
// The reusable-components `Form` builds `new FormData(formElement)` on
|
|
319
|
+
// submit, which the unit jsdom env's undici FormData rejects; install the
|
|
320
|
+
// shared shim that reads the form's successful controls.
|
|
321
|
+
installFormDataShim();
|
|
322
|
+
it("fetches registrations on mount, renders task controls, and registers a library on click", async () => {
|
|
323
|
+
const user = userEvent.setup();
|
|
324
|
+
const registerLibrary = jest.fn().mockResolvedValue(undefined);
|
|
325
|
+
const fetchLibraryRegistrations = jest.fn().mockResolvedValue(undefined);
|
|
326
|
+
renderWithContext(
|
|
327
|
+
<Collections
|
|
328
|
+
data={
|
|
329
|
+
{
|
|
330
|
+
collections: [
|
|
331
|
+
{
|
|
332
|
+
id: 7,
|
|
333
|
+
protocol: "OPDS Import",
|
|
334
|
+
name: "An OPDS Collection",
|
|
335
|
+
libraries: [],
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
protocols: [
|
|
339
|
+
{
|
|
340
|
+
name: "OPDS Import",
|
|
341
|
+
supports_registration: true,
|
|
342
|
+
supports_import: true,
|
|
343
|
+
settings: [],
|
|
344
|
+
library_settings: [],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
allLibraries,
|
|
348
|
+
} as any
|
|
349
|
+
}
|
|
350
|
+
editOrCreate="edit"
|
|
351
|
+
identifier="7"
|
|
352
|
+
fetchData={jest.fn()}
|
|
353
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
354
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
355
|
+
registerLibrary={registerLibrary}
|
|
356
|
+
fetchLibraryRegistrations={fetchLibraryRegistrations}
|
|
357
|
+
importCollection={jest.fn().mockResolvedValue(undefined)}
|
|
358
|
+
reapCollection={jest.fn().mockResolvedValue(undefined)}
|
|
359
|
+
csrfToken="token"
|
|
360
|
+
isFetching={false}
|
|
361
|
+
/>,
|
|
362
|
+
sysAdminConfig
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// componentDidMount fetches the registrations once. (CollectionEditForm's
|
|
366
|
+
// renderAdditionalContent also runs here, since the edited collection has
|
|
367
|
+
// an id, injecting the import/reap task panels into the form.)
|
|
368
|
+
expect(fetchLibraryRegistrations).toHaveBeenCalledTimes(1);
|
|
369
|
+
|
|
370
|
+
// Registering a library goes through getChildContext().registerLibrary.
|
|
371
|
+
expect(registerLibrary).not.toHaveBeenCalled();
|
|
372
|
+
await user.click(screen.getByRole("button", { name: "Register" }));
|
|
373
|
+
|
|
374
|
+
expect(registerLibrary).toHaveBeenCalledTimes(1);
|
|
375
|
+
const formData = registerLibrary.mock.calls[0][0];
|
|
376
|
+
expect(formData.get("library_short_name")).toBe("nypl");
|
|
377
|
+
expect(formData.get("collection_id")).toBe("7");
|
|
378
|
+
|
|
379
|
+
// After a successful registration, the child context refetches.
|
|
380
|
+
await waitFor(() =>
|
|
381
|
+
expect(fetchLibraryRegistrations).toHaveBeenCalledTimes(2)
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("confirm before disassociating libraries", () => {
|
|
387
|
+
const initialLibraries = [
|
|
388
|
+
{ short_name: "palace", name: "Palace" },
|
|
389
|
+
{ short_name: "another-library", name: "Another Library" },
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const renderEditForm = (
|
|
393
|
+
libraries: Array<{ short_name: string }>,
|
|
394
|
+
libraryMeta: Array<{ short_name: string; name?: string }>
|
|
395
|
+
) =>
|
|
396
|
+
renderWithContext(
|
|
397
|
+
<Collections
|
|
398
|
+
data={
|
|
399
|
+
{
|
|
400
|
+
collections: [
|
|
401
|
+
{
|
|
402
|
+
id: 7,
|
|
403
|
+
protocol: "OPDS Import",
|
|
404
|
+
name: "An OPDS Collection",
|
|
405
|
+
libraries,
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
protocols: [
|
|
409
|
+
{ name: "OPDS Import", settings: [], library_settings: [] },
|
|
410
|
+
],
|
|
411
|
+
allLibraries: libraryMeta,
|
|
412
|
+
} as any
|
|
413
|
+
}
|
|
414
|
+
editOrCreate="edit"
|
|
415
|
+
identifier="7"
|
|
416
|
+
fetchData={jest.fn()}
|
|
417
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
418
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
419
|
+
registerLibrary={jest.fn().mockResolvedValue(undefined)}
|
|
420
|
+
importCollection={jest.fn().mockResolvedValue(undefined)}
|
|
421
|
+
reapCollection={jest.fn().mockResolvedValue(undefined)}
|
|
422
|
+
csrfToken="token"
|
|
423
|
+
isFetching={false}
|
|
424
|
+
/>,
|
|
425
|
+
sysAdminConfig
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
it("removes the library when the confirmation is accepted", () => {
|
|
429
|
+
const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
430
|
+
const { container } = renderEditForm(
|
|
431
|
+
[...initialLibraries],
|
|
432
|
+
[...initialLibraries]
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// One remove button per associated library.
|
|
436
|
+
expect(container.querySelectorAll("button.remove-btn")).toHaveLength(2);
|
|
437
|
+
|
|
438
|
+
fireEvent.click(container.querySelectorAll("button.remove-btn")[0]);
|
|
439
|
+
|
|
440
|
+
// The prompt names the (full) library being removed.
|
|
441
|
+
expect(confirmSpy).toHaveBeenCalledTimes(1);
|
|
442
|
+
expect(confirmSpy.mock.calls[0][0]).toBe(
|
|
443
|
+
'Disassociating library "Palace" from this collection will ' +
|
|
444
|
+
"remove all loans and holds for its patrons. Do you wish to continue?"
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// The association (and its remove button) is gone.
|
|
448
|
+
expect(container.querySelectorAll("button.remove-btn")).toHaveLength(1);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("keeps the library when the confirmation is canceled", () => {
|
|
452
|
+
jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
453
|
+
const { container } = renderEditForm(
|
|
454
|
+
[...initialLibraries],
|
|
455
|
+
[...initialLibraries]
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(container.querySelectorAll("button.remove-btn")).toHaveLength(2);
|
|
459
|
+
fireEvent.click(container.querySelectorAll("button.remove-btn")[0]);
|
|
460
|
+
// Nothing was removed.
|
|
461
|
+
expect(container.querySelectorAll("button.remove-btn")).toHaveLength(2);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("uses the library short_name in the prompt when the full name is unavailable", () => {
|
|
465
|
+
const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
466
|
+
// The library is not present in allLibraries, so getLibrary() returns null.
|
|
467
|
+
const { container } = renderEditForm([{ short_name: "orphan" }], []);
|
|
468
|
+
|
|
469
|
+
fireEvent.click(container.querySelector("button.remove-btn"));
|
|
470
|
+
|
|
471
|
+
expect(confirmSpy).toHaveBeenCalledTimes(1);
|
|
472
|
+
expect(confirmSpy.mock.calls[0][0]).toBe(
|
|
473
|
+
'Disassociating library "orphan" from this collection will ' +
|
|
474
|
+
"remove all loans and holds for its patrons. Do you wish to continue?"
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe("create mode", () => {
|
|
480
|
+
it("renders the create form without import/reap task panels", () => {
|
|
481
|
+
// In create mode there is no saved collection, so CollectionEditForm's
|
|
482
|
+
// renderAdditionalContent short-circuits and injects no task panels.
|
|
483
|
+
renderWithContext(
|
|
484
|
+
<Collections
|
|
485
|
+
data={
|
|
486
|
+
{
|
|
487
|
+
collections: [],
|
|
488
|
+
protocols: [
|
|
489
|
+
{ name: "OPDS Import", settings: [], library_settings: [] },
|
|
490
|
+
],
|
|
491
|
+
allLibraries,
|
|
492
|
+
} as any
|
|
493
|
+
}
|
|
494
|
+
editOrCreate="create"
|
|
495
|
+
fetchData={jest.fn()}
|
|
496
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
497
|
+
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
498
|
+
registerLibrary={jest.fn().mockResolvedValue(undefined)}
|
|
499
|
+
importCollection={jest.fn().mockResolvedValue(undefined)}
|
|
500
|
+
reapCollection={jest.fn().mockResolvedValue(undefined)}
|
|
501
|
+
csrfToken="token"
|
|
502
|
+
isFetching={false}
|
|
503
|
+
/>,
|
|
504
|
+
sysAdminConfig
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
expect(
|
|
508
|
+
screen.getByRole("heading", { name: "Create a new collection" })
|
|
509
|
+
).toBeInTheDocument();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe("deletion", () => {
|
|
514
|
+
it("deletes a collection and refetches after confirmation", async () => {
|
|
515
|
+
const deleteItem = jest.fn().mockResolvedValue(undefined);
|
|
516
|
+
const fetchData = jest.fn();
|
|
517
|
+
const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true);
|
|
518
|
+
|
|
519
|
+
const { container } = renderWithContext(
|
|
520
|
+
<Collections
|
|
521
|
+
data={
|
|
522
|
+
{
|
|
523
|
+
collections: [{ id: 1, protocol: "p", name: "My Collection" }],
|
|
524
|
+
protocols: [],
|
|
525
|
+
allLibraries,
|
|
526
|
+
} as any
|
|
527
|
+
}
|
|
528
|
+
fetchData={fetchData}
|
|
529
|
+
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
530
|
+
deleteItem={deleteItem}
|
|
531
|
+
registerLibrary={jest.fn().mockResolvedValue(undefined)}
|
|
532
|
+
importCollection={jest.fn().mockResolvedValue(undefined)}
|
|
533
|
+
reapCollection={jest.fn().mockResolvedValue(undefined)}
|
|
534
|
+
csrfToken="token"
|
|
535
|
+
isFetching={false}
|
|
536
|
+
/>,
|
|
537
|
+
sysAdminConfig
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
fireEvent.click(container.querySelector("button.delete-item"));
|
|
541
|
+
|
|
542
|
+
expect(confirmSpy).toHaveBeenCalledTimes(1);
|
|
543
|
+
await waitFor(() => expect(deleteItem).toHaveBeenCalledWith(1));
|
|
544
|
+
// fetchData is called once on mount; the post-delete refetch is the second
|
|
545
|
+
// call, so assert the count rather than mere invocation.
|
|
546
|
+
await waitFor(() => expect(fetchData).toHaveBeenCalledTimes(2));
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import ColorPicker from "../../../src/components/ColorPicker";
|
|
6
|
+
|
|
7
|
+
describe("ColorPicker", () => {
|
|
8
|
+
const setting = {
|
|
9
|
+
key: "key",
|
|
10
|
+
label: "label",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it("should show the compact picker", () => {
|
|
14
|
+
const { container } = render(
|
|
15
|
+
<ColorPicker setting={setting} value="#123456" />
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// The react-color CompactPicker renders a `.compact-picker` container whose
|
|
19
|
+
// hex field reflects the color it was given.
|
|
20
|
+
expect(container.querySelector(".compact-picker")).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByLabelText("hex")).toHaveValue("#123456");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should have a hidden input with the value", () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<ColorPicker setting={setting} value="#123456" />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
30
|
+
'input[type="hidden"]'
|
|
31
|
+
);
|
|
32
|
+
expect(input).toHaveAttribute("name", "key");
|
|
33
|
+
expect(input).toHaveValue("#123456");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should change the value", async () => {
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
// getValue() is the imperative API ProtocolFormField calls via a ref in
|
|
39
|
+
// production, so exercise it through a real ref here.
|
|
40
|
+
const ref = React.createRef<ColorPicker>();
|
|
41
|
+
const { container } = render(
|
|
42
|
+
<ColorPicker ref={ref} setting={setting} value="#123456" />
|
|
43
|
+
);
|
|
44
|
+
const hiddenInput = container.querySelector<HTMLInputElement>(
|
|
45
|
+
'input[type="hidden"]'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(ref.current.getValue()).toBe("#123456");
|
|
49
|
+
expect(hiddenInput).toHaveValue("#123456");
|
|
50
|
+
|
|
51
|
+
// Clicking a palette swatch fires the picker's onChangeComplete, which
|
|
52
|
+
// updates the component's value (react-color debounces that callback).
|
|
53
|
+
await user.click(screen.getByTitle("#FE9200"));
|
|
54
|
+
|
|
55
|
+
await waitFor(() => expect(ref.current.getValue()).toBe("#fe9200"));
|
|
56
|
+
expect(hiddenInput).toHaveValue("#fe9200");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { installFormDataShim } from "../testUtils/formDataShim";
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import ComplaintForm from "../../../src/components/ComplaintForm";
|
|
7
|
+
installFormDataShim();
|
|
8
|
+
|
|
9
|
+
// ComplaintForm's default export is a plain (unconnected) class component, so a
|
|
10
|
+
// bare `render` is sufficient — no providers/store/context are required.
|
|
11
|
+
const renderForm = (
|
|
12
|
+
props: Partial<React.ComponentProps<typeof ComplaintForm>> = {}
|
|
13
|
+
) =>
|
|
14
|
+
render(
|
|
15
|
+
<ComplaintForm
|
|
16
|
+
disabled={false}
|
|
17
|
+
complaintUrl="complaint url"
|
|
18
|
+
postComplaint={jest.fn().mockResolvedValue(undefined)}
|
|
19
|
+
refreshComplaints={jest.fn()}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const getSelect = (container: HTMLElement) =>
|
|
25
|
+
container.querySelector("select") as HTMLSelectElement;
|
|
26
|
+
|
|
27
|
+
describe("ComplaintForm", () => {
|
|
28
|
+
describe("rendering", () => {
|
|
29
|
+
it("shows a select field with a default value", () => {
|
|
30
|
+
const { container } = renderForm();
|
|
31
|
+
const select = getSelect(container);
|
|
32
|
+
expect(select).toBeInTheDocument();
|
|
33
|
+
expect(select).not.toBeDisabled();
|
|
34
|
+
expect(screen.getAllByRole("option")[0]).toHaveTextContent(
|
|
35
|
+
"Complaint type"
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("shows complaint type options", () => {
|
|
40
|
+
renderForm();
|
|
41
|
+
const values = screen
|
|
42
|
+
.getAllByRole("option")
|
|
43
|
+
.map((option) => (option as HTMLOptionElement).value);
|
|
44
|
+
expect(values).toStrictEqual([
|
|
45
|
+
"",
|
|
46
|
+
"cannot-issue-loan",
|
|
47
|
+
"cannot-render",
|
|
48
|
+
"wrong-title",
|
|
49
|
+
"wrong-author",
|
|
50
|
+
"wrong-audience",
|
|
51
|
+
"cannot-fulfill-loan",
|
|
52
|
+
"bad-description",
|
|
53
|
+
"cannot-return",
|
|
54
|
+
"bad-cover-image",
|
|
55
|
+
"wrong-medium",
|
|
56
|
+
"wrong-age-range",
|
|
57
|
+
"wrong-genre",
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("formats complaint type options", () => {
|
|
62
|
+
renderForm();
|
|
63
|
+
const texts = screen
|
|
64
|
+
.getAllByRole("option")
|
|
65
|
+
.map((option) => option.textContent);
|
|
66
|
+
expect(texts).toStrictEqual([
|
|
67
|
+
"Complaint type",
|
|
68
|
+
"Cannot issue loan",
|
|
69
|
+
"Cannot render",
|
|
70
|
+
"Wrong title",
|
|
71
|
+
"Wrong author",
|
|
72
|
+
"Wrong audience",
|
|
73
|
+
"Cannot fulfill loan",
|
|
74
|
+
"Bad description",
|
|
75
|
+
"Cannot return",
|
|
76
|
+
"Bad cover image",
|
|
77
|
+
"Wrong medium",
|
|
78
|
+
"Wrong age range",
|
|
79
|
+
"Wrong genre",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("shows a submit button", () => {
|
|
84
|
+
renderForm();
|
|
85
|
+
expect(
|
|
86
|
+
screen.getByRole("button", { name: "Submit" })
|
|
87
|
+
).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("disables the select and submit button when disabled", () => {
|
|
91
|
+
const { container } = renderForm({ disabled: true });
|
|
92
|
+
expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled();
|
|
93
|
+
expect(getSelect(container)).toBeDisabled();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("behavior", () => {
|
|
98
|
+
it("posts complaints", async () => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
const postComplaint = jest.fn().mockResolvedValue(undefined);
|
|
101
|
+
const { container } = renderForm({ postComplaint });
|
|
102
|
+
|
|
103
|
+
await user.selectOptions(getSelect(container), "bad-description");
|
|
104
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
105
|
+
|
|
106
|
+
expect(postComplaint).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(postComplaint.mock.calls[0][0]).toBe("complaint url");
|
|
108
|
+
expect(postComplaint.mock.calls[0][1].type).toBe(
|
|
109
|
+
"http://librarysimplified.org/terms/problem/bad-description"
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("refreshes complaints after a successful post", async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
const refreshComplaints = jest.fn();
|
|
116
|
+
const { container } = renderForm({
|
|
117
|
+
postComplaint: jest.fn().mockResolvedValue(undefined),
|
|
118
|
+
refreshComplaints,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await user.selectOptions(getSelect(container), "bad-description");
|
|
122
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
123
|
+
|
|
124
|
+
await waitFor(() => expect(refreshComplaints).toHaveBeenCalledTimes(1));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("clears the selected complaint type after a successful post", async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
const { container } = renderForm({
|
|
130
|
+
postComplaint: jest.fn().mockResolvedValue(undefined),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const select = getSelect(container);
|
|
134
|
+
await user.selectOptions(select, "bad-description");
|
|
135
|
+
expect(select).toHaveValue("bad-description");
|
|
136
|
+
|
|
137
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
138
|
+
|
|
139
|
+
await waitFor(() => expect(getSelect(container)).toHaveValue(""));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("displays an error if no type is selected", async () => {
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
const postComplaint = jest.fn().mockResolvedValue(undefined);
|
|
145
|
+
renderForm({ postComplaint });
|
|
146
|
+
|
|
147
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
148
|
+
|
|
149
|
+
expect(
|
|
150
|
+
await screen.findByText("You must select a complaint type!")
|
|
151
|
+
).toBeInTheDocument();
|
|
152
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
153
|
+
"You must select a complaint type!"
|
|
154
|
+
);
|
|
155
|
+
expect(postComplaint).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("shows an error message if the post fails", async () => {
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
const postComplaint = jest.fn().mockRejectedValue(new Error("nope"));
|
|
161
|
+
const { container } = renderForm({ postComplaint });
|
|
162
|
+
|
|
163
|
+
await user.selectOptions(getSelect(container), "bad-description");
|
|
164
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
165
|
+
|
|
166
|
+
expect(
|
|
167
|
+
await screen.findByText("Couldn't post complaint.")
|
|
168
|
+
).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|