@thepalaceproject/circulation-admin 1.43.0 → 1.44.0-post.10
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,18 +1,15 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { fireEvent } from "@testing-library/react";
|
|
2
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
3
4
|
import {
|
|
4
5
|
EditableConfigList,
|
|
5
6
|
EditFormProps,
|
|
7
|
+
AdditionalContentProps,
|
|
6
8
|
} from "../../../src/components/EditableConfigList";
|
|
7
9
|
import renderWithContext from "../testUtils/renderWithContext";
|
|
8
10
|
import { ConfigurationSettings } from "../../../src/interfaces";
|
|
9
11
|
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
10
|
-
|
|
11
|
-
// NB: This adds tests to the already existing tests in:
|
|
12
|
-
// - `src/components/__tests__/EditableConfigList-test.tsx`.
|
|
13
|
-
//
|
|
14
|
-
// Those tests should eventually be migrated here and
|
|
15
|
-
// adapted to the Jest/React Testing Library paradigm.
|
|
12
|
+
import * as navigate from "../../../src/utils/navigate";
|
|
16
13
|
|
|
17
14
|
describe("EditableConfigList - library association disclosure", () => {
|
|
18
15
|
// ── Test doubles ──────────────────────────────────────────────────────────
|
|
@@ -383,7 +380,11 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
383
380
|
it("falls back to short_name when allLibraries is absent from the data", () => {
|
|
384
381
|
const { container } = renderWithContext(
|
|
385
382
|
<TestServiceList
|
|
386
|
-
data={{
|
|
383
|
+
data={{
|
|
384
|
+
services: [
|
|
385
|
+
{ id: 1, name: "Service A", libraries: [{ short_name: "nypl" }] },
|
|
386
|
+
],
|
|
387
|
+
}}
|
|
387
388
|
fetchData={jest.fn()}
|
|
388
389
|
editItem={jest.fn().mockResolvedValue(undefined)}
|
|
389
390
|
deleteItem={jest.fn().mockResolvedValue(undefined)}
|
|
@@ -393,7 +394,603 @@ describe("EditableConfigList - library association disclosure", () => {
|
|
|
393
394
|
config
|
|
394
395
|
);
|
|
395
396
|
fireEvent.click(container.querySelector(".association-toggle"));
|
|
396
|
-
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
397
|
+
expect(container.querySelector(".associated-items li").textContent).toBe(
|
|
398
|
+
"nypl"
|
|
399
|
+
);
|
|
397
400
|
});
|
|
398
401
|
});
|
|
399
402
|
});
|
|
403
|
+
|
|
404
|
+
// These exercise the abstract base class through concrete test subclasses,
|
|
405
|
+
// asserting observable DOM instead of reaching into instances.
|
|
406
|
+
describe("EditableConfigList - base class via test subclasses", () => {
|
|
407
|
+
interface Thing {
|
|
408
|
+
id: number;
|
|
409
|
+
label: string;
|
|
410
|
+
level?: number;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface Things {
|
|
414
|
+
things: Thing[];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Closure-controlled permission flags.
|
|
418
|
+
let canCreate: boolean;
|
|
419
|
+
let canDelete: boolean;
|
|
420
|
+
let canEdit: boolean;
|
|
421
|
+
|
|
422
|
+
// A test EditForm that surfaces the props the base class passes to it, so
|
|
423
|
+
// prop-only assertions can be checked against the rendered DOM.
|
|
424
|
+
class ThingEditForm extends React.Component<EditFormProps<Things, Thing>> {
|
|
425
|
+
render(): JSX.Element {
|
|
426
|
+
const { disabled, save, item, listDataKey, adminLevel } = this.props;
|
|
427
|
+
return (
|
|
428
|
+
<div className="thing-edit-form">
|
|
429
|
+
<span data-testid="ef-disabled">{String(disabled)}</span>
|
|
430
|
+
<span data-testid="ef-has-save">{String(!!save)}</span>
|
|
431
|
+
<span data-testid="ef-datakey">{listDataKey}</span>
|
|
432
|
+
<span data-testid="ef-item">{item ? String(item.id) : "none"}</span>
|
|
433
|
+
<span data-testid="ef-adminlevel">{String(adminLevel)}</span>
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
data-testid="ef-save"
|
|
437
|
+
onClick={() => save && save(new FormData())}
|
|
438
|
+
>
|
|
439
|
+
submit form
|
|
440
|
+
</button>
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
class ThingAdditionalContent extends React.Component<
|
|
447
|
+
AdditionalContentProps<Things, Thing>
|
|
448
|
+
> {
|
|
449
|
+
render(): JSX.Element {
|
|
450
|
+
return (
|
|
451
|
+
<div
|
|
452
|
+
className="additional-content"
|
|
453
|
+
data-item-id={String((this.props.item as any)?.id)}
|
|
454
|
+
data-csrf={this.props.csrfToken}
|
|
455
|
+
>
|
|
456
|
+
Test Additional Content
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Uses the base implementations of canCreate/canDelete/canEdit.
|
|
463
|
+
class BaseThingList extends EditableConfigList<Things, Thing> {
|
|
464
|
+
EditForm = ThingEditForm;
|
|
465
|
+
listDataKey = "things";
|
|
466
|
+
itemTypeName = "thing";
|
|
467
|
+
urlBase = "/admin/things/";
|
|
468
|
+
identifierKey = "id";
|
|
469
|
+
labelKey = "label";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Overrides permission checks from the closure flags.
|
|
473
|
+
class ThingEditableConfigList extends BaseThingList {
|
|
474
|
+
label(item): string {
|
|
475
|
+
return "test " + super.label(item);
|
|
476
|
+
}
|
|
477
|
+
canCreate() {
|
|
478
|
+
return canCreate;
|
|
479
|
+
}
|
|
480
|
+
canDelete() {
|
|
481
|
+
return canDelete;
|
|
482
|
+
}
|
|
483
|
+
canEdit() {
|
|
484
|
+
return canEdit;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
class OneThingEditableConfigList extends ThingEditableConfigList {
|
|
489
|
+
limitOne = true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
class ThingAdditionalContentEditableConfigList extends ThingEditableConfigList {
|
|
493
|
+
AdditionalContent = ThingAdditionalContent;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
class CapsThingList extends BaseThingList {
|
|
497
|
+
itemTypeName = "CDN";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
class ThingWithSelfTests extends ThingEditableConfigList {
|
|
501
|
+
links = {
|
|
502
|
+
info: (
|
|
503
|
+
<>
|
|
504
|
+
Self-tests for the things have been moved to{" "}
|
|
505
|
+
<a href="/admin/web/troubleshooting/self-tests/thingServices">
|
|
506
|
+
the troubleshooting page
|
|
507
|
+
</a>
|
|
508
|
+
.
|
|
509
|
+
</>
|
|
510
|
+
),
|
|
511
|
+
footer: (
|
|
512
|
+
<>
|
|
513
|
+
Problems with your things? Please visit{" "}
|
|
514
|
+
<a href="/admin/web/troubleshooting/self-tests/thingServices">
|
|
515
|
+
the troubleshooting page
|
|
516
|
+
</a>
|
|
517
|
+
.
|
|
518
|
+
</>
|
|
519
|
+
),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const thingData: Thing = { id: 5, label: "label" };
|
|
524
|
+
const thingsData: Things = { things: [thingData] };
|
|
525
|
+
|
|
526
|
+
let fetchData: jest.Mock;
|
|
527
|
+
let editItem: jest.Mock;
|
|
528
|
+
let deleteItem: jest.Mock;
|
|
529
|
+
|
|
530
|
+
const systemConfig: Partial<ConfigurationSettings> = {
|
|
531
|
+
csrfToken: "token",
|
|
532
|
+
featureFlags: defaultFeatureFlags,
|
|
533
|
+
roles: [{ role: "system", library: "nypl" }],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const configWithRoles = (
|
|
537
|
+
roles: Partial<ConfigurationSettings>["roles"]
|
|
538
|
+
): Partial<ConfigurationSettings> => ({
|
|
539
|
+
csrfToken: "token",
|
|
540
|
+
featureFlags: defaultFeatureFlags,
|
|
541
|
+
roles,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const baseProps = (overrides: Record<string, any> = {}) => ({
|
|
545
|
+
data: thingsData,
|
|
546
|
+
fetchData,
|
|
547
|
+
editItem,
|
|
548
|
+
deleteItem,
|
|
549
|
+
csrfToken: "token",
|
|
550
|
+
isFetching: false,
|
|
551
|
+
...overrides,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
beforeEach(() => {
|
|
555
|
+
fetchData = jest.fn();
|
|
556
|
+
editItem = jest.fn().mockResolvedValue(undefined);
|
|
557
|
+
deleteItem = jest.fn().mockResolvedValue(undefined);
|
|
558
|
+
canCreate = true;
|
|
559
|
+
canDelete = true;
|
|
560
|
+
canEdit = true;
|
|
561
|
+
jest.spyOn(window, "scrollTo").mockImplementation(() => undefined);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
afterEach(() => {
|
|
565
|
+
jest.restoreAllMocks();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("shows an error message if there's a problem loading the list", () => {
|
|
569
|
+
const { container, rerender } = renderWithContext(
|
|
570
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
571
|
+
systemConfig
|
|
572
|
+
);
|
|
573
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
574
|
+
|
|
575
|
+
const fetchError = {
|
|
576
|
+
status: 404,
|
|
577
|
+
response: "test load error",
|
|
578
|
+
url: "test url",
|
|
579
|
+
};
|
|
580
|
+
rerender(<ThingEditableConfigList {...baseProps({ fetchError })} />);
|
|
581
|
+
expect(container.querySelector(".alert-danger")).not.toBeNull();
|
|
582
|
+
expect(screen.getByText("Error: test load error")).toBeInTheDocument();
|
|
583
|
+
|
|
584
|
+
// Hidden when the user goes to the form.
|
|
585
|
+
rerender(
|
|
586
|
+
<ThingEditableConfigList
|
|
587
|
+
{...baseProps({ fetchError, editOrCreate: "create" })}
|
|
588
|
+
/>
|
|
589
|
+
);
|
|
590
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("shows form submission error message only if the form is displayed", () => {
|
|
594
|
+
const formError = {
|
|
595
|
+
status: 400,
|
|
596
|
+
response: "test submission error",
|
|
597
|
+
url: "test url",
|
|
598
|
+
};
|
|
599
|
+
const { container, rerender } = renderWithContext(
|
|
600
|
+
<ThingEditableConfigList {...baseProps({ formError })} />,
|
|
601
|
+
systemConfig
|
|
602
|
+
);
|
|
603
|
+
// Not displayed while looking at the list.
|
|
604
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
605
|
+
|
|
606
|
+
rerender(
|
|
607
|
+
<ThingEditableConfigList
|
|
608
|
+
{...baseProps({ formError, editOrCreate: "create" })}
|
|
609
|
+
/>
|
|
610
|
+
);
|
|
611
|
+
expect(container.querySelector(".alert-danger")).not.toBeNull();
|
|
612
|
+
expect(
|
|
613
|
+
screen.getByText("Error: test submission error")
|
|
614
|
+
).toBeInTheDocument();
|
|
615
|
+
|
|
616
|
+
// Hidden when the user goes back to the list.
|
|
617
|
+
rerender(
|
|
618
|
+
<ThingEditableConfigList
|
|
619
|
+
{...baseProps({ formError, editOrCreate: "" })}
|
|
620
|
+
/>
|
|
621
|
+
);
|
|
622
|
+
expect(container.querySelector(".alert-danger")).toBeNull();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("shows a success message with an edit link on create", () => {
|
|
626
|
+
const { container } = renderWithContext(
|
|
627
|
+
<ThingEditableConfigList
|
|
628
|
+
{...baseProps({ responseBody: "itemType", editOrCreate: "create" })}
|
|
629
|
+
/>,
|
|
630
|
+
systemConfig
|
|
631
|
+
);
|
|
632
|
+
const success = container.querySelector(".alert-success");
|
|
633
|
+
expect(success).not.toBeNull();
|
|
634
|
+
expect(success.textContent).toBe("Successfully created a new thing");
|
|
635
|
+
const link = success.querySelector<HTMLAnchorElement>("a");
|
|
636
|
+
expect(link).not.toBeNull();
|
|
637
|
+
expect(link.getAttribute("href")).toBe("/admin/things/edit/itemType");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("shows a success message without a link on edit", () => {
|
|
641
|
+
const { container } = renderWithContext(
|
|
642
|
+
<ThingEditableConfigList
|
|
643
|
+
{...baseProps({ responseBody: "itemType", editOrCreate: "edit" })}
|
|
644
|
+
/>,
|
|
645
|
+
systemConfig
|
|
646
|
+
);
|
|
647
|
+
const success = container.querySelector(".alert-success");
|
|
648
|
+
expect(success).not.toBeNull();
|
|
649
|
+
expect(success.textContent).toBe("Successfully edited this thing");
|
|
650
|
+
expect(success.querySelector("a")).toBeNull();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("keeps all-caps item type names uppercase in the success message", () => {
|
|
654
|
+
const { container } = renderWithContext(
|
|
655
|
+
<CapsThingList
|
|
656
|
+
{...baseProps({ responseBody: "itemType", editOrCreate: "create" })}
|
|
657
|
+
/>,
|
|
658
|
+
systemConfig
|
|
659
|
+
);
|
|
660
|
+
const success = container.querySelector(".alert-success");
|
|
661
|
+
expect(success.textContent).toBe("Successfully created a new CDN");
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("shows a loading indicator only while fetching", () => {
|
|
665
|
+
const { rerender } = renderWithContext(
|
|
666
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
667
|
+
systemConfig
|
|
668
|
+
);
|
|
669
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
670
|
+
rerender(<ThingEditableConfigList {...baseProps({ isFetching: true })} />);
|
|
671
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("shows the thing header", () => {
|
|
675
|
+
renderWithContext(
|
|
676
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
677
|
+
systemConfig
|
|
678
|
+
);
|
|
679
|
+
expect(
|
|
680
|
+
screen.getByRole("heading", { level: 2, name: "Thing configuration" })
|
|
681
|
+
).toBeInTheDocument();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("shows the thing list with an edit link and a count", () => {
|
|
685
|
+
const { container } = renderWithContext(
|
|
686
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
687
|
+
systemConfig
|
|
688
|
+
);
|
|
689
|
+
const items = container.querySelectorAll("ul > li");
|
|
690
|
+
expect(items).toHaveLength(1);
|
|
691
|
+
expect(items[0].textContent).toContain("test label");
|
|
692
|
+
const editLink = items[0].querySelector<HTMLAnchorElement>(".edit-item");
|
|
693
|
+
expect(editLink.getAttribute("href")).toBe("/admin/things/edit/5");
|
|
694
|
+
expect(
|
|
695
|
+
container.querySelector(".list-container header div").textContent
|
|
696
|
+
).toBe("1 configured");
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("updates the thing list when new data arrives", () => {
|
|
700
|
+
const { container, rerender } = renderWithContext(
|
|
701
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
702
|
+
systemConfig
|
|
703
|
+
);
|
|
704
|
+
const newThingsData = {
|
|
705
|
+
things: [thingData, { id: 6, label: "another thing" }],
|
|
706
|
+
};
|
|
707
|
+
rerender(
|
|
708
|
+
<ThingEditableConfigList {...baseProps({ data: newThingsData })} />
|
|
709
|
+
);
|
|
710
|
+
const items = container.querySelectorAll("ul > li");
|
|
711
|
+
expect(items).toHaveLength(2);
|
|
712
|
+
expect(items[1].textContent).toContain("test another thing");
|
|
713
|
+
expect(
|
|
714
|
+
items[1]
|
|
715
|
+
.querySelector<HTMLAnchorElement>(".edit-item")
|
|
716
|
+
.getAttribute("href")
|
|
717
|
+
).toBe("/admin/things/edit/6");
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("shows the create link, which respects canCreate", () => {
|
|
721
|
+
const { container, rerender } = renderWithContext(
|
|
722
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
723
|
+
systemConfig
|
|
724
|
+
);
|
|
725
|
+
const createLink =
|
|
726
|
+
container.querySelector<HTMLAnchorElement>(".create-item");
|
|
727
|
+
expect(createLink.textContent).toBe("Create new thing");
|
|
728
|
+
expect(createLink.getAttribute("href")).toBe("/admin/things/create");
|
|
729
|
+
|
|
730
|
+
canCreate = false;
|
|
731
|
+
rerender(<ThingEditableConfigList {...baseProps()} />);
|
|
732
|
+
expect(container.querySelector(".create-item")).toBeNull();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("uses the base canCreate (true) when a subclass does not override it", () => {
|
|
736
|
+
const { container } = renderWithContext(
|
|
737
|
+
<BaseThingList {...baseProps()} />,
|
|
738
|
+
systemConfig
|
|
739
|
+
);
|
|
740
|
+
expect(container.querySelector(".create-item")).not.toBeNull();
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("hides the create link if only one item is allowed and it already exists", () => {
|
|
744
|
+
const { container, rerender } = renderWithContext(
|
|
745
|
+
<OneThingEditableConfigList {...baseProps()} />,
|
|
746
|
+
systemConfig
|
|
747
|
+
);
|
|
748
|
+
expect(container.querySelector(".create-item")).toBeNull();
|
|
749
|
+
|
|
750
|
+
rerender(
|
|
751
|
+
<OneThingEditableConfigList {...baseProps({ data: { things: [] } })} />
|
|
752
|
+
);
|
|
753
|
+
const createLink =
|
|
754
|
+
container.querySelector<HTMLAnchorElement>(".create-item");
|
|
755
|
+
expect(createLink).not.toBeNull();
|
|
756
|
+
expect(createLink.getAttribute("href")).toBe("/admin/things/create");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("shows a view button instead of an edit button when canEdit is false", () => {
|
|
760
|
+
canEdit = false;
|
|
761
|
+
const { container } = renderWithContext(
|
|
762
|
+
<ThingEditableConfigList
|
|
763
|
+
{...baseProps({
|
|
764
|
+
data: { things: [{ id: 6, label: "View Only", level: 3 }] },
|
|
765
|
+
})}
|
|
766
|
+
/>,
|
|
767
|
+
configWithRoles([{ role: "manager", library: "nypl" }])
|
|
768
|
+
);
|
|
769
|
+
const editItemLink = container.querySelector(".edit-item");
|
|
770
|
+
expect(editItemLink.textContent).toContain("View");
|
|
771
|
+
expect(editItemLink.textContent).not.toContain("Edit");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("shows a delete button when canDelete is true and hides it otherwise", () => {
|
|
775
|
+
// Base canDelete: system admin (level 3) → delete button shown.
|
|
776
|
+
const shown = renderWithContext(
|
|
777
|
+
<BaseThingList {...baseProps()} />,
|
|
778
|
+
systemConfig
|
|
779
|
+
);
|
|
780
|
+
expect(shown.container.querySelector(".delete-item")).not.toBeNull();
|
|
781
|
+
shown.unmount();
|
|
782
|
+
|
|
783
|
+
// Base canDelete for a librarian (level 1) → no delete button.
|
|
784
|
+
const hiddenByRole = renderWithContext(
|
|
785
|
+
<BaseThingList {...baseProps()} />,
|
|
786
|
+
configWithRoles([{ role: "librarian", library: "nypl" }])
|
|
787
|
+
);
|
|
788
|
+
expect(hiddenByRole.container.querySelector(".delete-item")).toBeNull();
|
|
789
|
+
hiddenByRole.unmount();
|
|
790
|
+
|
|
791
|
+
// Explicit override returning false → no delete button.
|
|
792
|
+
canDelete = false;
|
|
793
|
+
const hiddenByOverride = renderWithContext(
|
|
794
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
795
|
+
systemConfig
|
|
796
|
+
);
|
|
797
|
+
expect(hiddenByOverride.container.querySelector(".delete-item")).toBeNull();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("deletes an item only after the user confirms", async () => {
|
|
801
|
+
const user = userEvent.setup();
|
|
802
|
+
const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(false);
|
|
803
|
+
const { container } = renderWithContext(
|
|
804
|
+
<BaseThingList {...baseProps()} />,
|
|
805
|
+
systemConfig
|
|
806
|
+
);
|
|
807
|
+
const deleteButton =
|
|
808
|
+
container.querySelector<HTMLButtonElement>(".delete-item");
|
|
809
|
+
expect(deleteButton).not.toBeNull();
|
|
810
|
+
|
|
811
|
+
await user.click(deleteButton);
|
|
812
|
+
expect(deleteItem).toHaveBeenCalledTimes(0);
|
|
813
|
+
|
|
814
|
+
confirmSpy.mockReturnValue(true);
|
|
815
|
+
await user.click(deleteButton);
|
|
816
|
+
await waitFor(() => expect(deleteItem).toHaveBeenCalledTimes(1));
|
|
817
|
+
expect(deleteItem).toHaveBeenCalledWith(5);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("renders the create form and its header", () => {
|
|
821
|
+
renderWithContext(
|
|
822
|
+
<ThingEditableConfigList {...baseProps({ editOrCreate: "create" })} />,
|
|
823
|
+
systemConfig
|
|
824
|
+
);
|
|
825
|
+
expect(
|
|
826
|
+
screen.getByRole("heading", { level: 3, name: "Create a new thing" })
|
|
827
|
+
).toBeInTheDocument();
|
|
828
|
+
expect(screen.getByTestId("ef-item")).toHaveTextContent("none");
|
|
829
|
+
expect(screen.getByTestId("ef-disabled")).toHaveTextContent("false");
|
|
830
|
+
expect(screen.getByTestId("ef-datakey")).toHaveTextContent("things");
|
|
831
|
+
expect(screen.getByTestId("ef-has-save")).toHaveTextContent("true");
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("renders the edit form with the item and an editable header", () => {
|
|
835
|
+
renderWithContext(
|
|
836
|
+
<ThingEditableConfigList
|
|
837
|
+
{...baseProps({ editOrCreate: "edit", identifier: "5" })}
|
|
838
|
+
/>,
|
|
839
|
+
systemConfig
|
|
840
|
+
);
|
|
841
|
+
expect(
|
|
842
|
+
screen.getByRole("heading", { level: 3, name: "Edit test label" })
|
|
843
|
+
).toBeInTheDocument();
|
|
844
|
+
expect(screen.getByTestId("ef-item")).toHaveTextContent("5");
|
|
845
|
+
expect(screen.getByTestId("ef-disabled")).toHaveTextContent("false");
|
|
846
|
+
expect(screen.getByTestId("ef-has-save")).toHaveTextContent("true");
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it("shows a non-editable header, no save function, and a disabled form when canEdit is false", () => {
|
|
850
|
+
canEdit = false;
|
|
851
|
+
renderWithContext(
|
|
852
|
+
<ThingEditableConfigList
|
|
853
|
+
{...baseProps({ editOrCreate: "edit", identifier: "5" })}
|
|
854
|
+
/>,
|
|
855
|
+
systemConfig
|
|
856
|
+
);
|
|
857
|
+
expect(
|
|
858
|
+
screen.getByRole("heading", { level: 3, name: "test label" })
|
|
859
|
+
).toBeInTheDocument();
|
|
860
|
+
expect(screen.getByTestId("ef-has-save")).toHaveTextContent("false");
|
|
861
|
+
expect(screen.getByTestId("ef-disabled")).toHaveTextContent("true");
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("updates the edit-form header when the item's label changes", () => {
|
|
865
|
+
const { rerender } = renderWithContext(
|
|
866
|
+
<ThingEditableConfigList
|
|
867
|
+
{...baseProps({ editOrCreate: "edit", identifier: "5" })}
|
|
868
|
+
/>,
|
|
869
|
+
systemConfig
|
|
870
|
+
);
|
|
871
|
+
expect(
|
|
872
|
+
screen.getByRole("heading", { level: 3, name: "Edit test label" })
|
|
873
|
+
).toBeInTheDocument();
|
|
874
|
+
|
|
875
|
+
rerender(
|
|
876
|
+
<ThingEditableConfigList
|
|
877
|
+
{...baseProps({
|
|
878
|
+
editOrCreate: "edit",
|
|
879
|
+
identifier: "5",
|
|
880
|
+
data: { things: [{ id: 5, label: "new thing!" }] },
|
|
881
|
+
})}
|
|
882
|
+
/>
|
|
883
|
+
);
|
|
884
|
+
expect(
|
|
885
|
+
screen.getByRole("heading", { level: 3, name: "Edit test new thing!" })
|
|
886
|
+
).toBeInTheDocument();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it("fetches on mount, passes a working save function, and refetches on save", async () => {
|
|
890
|
+
const user = userEvent.setup();
|
|
891
|
+
renderWithContext(
|
|
892
|
+
<ThingEditableConfigList {...baseProps({ editOrCreate: "create" })} />,
|
|
893
|
+
systemConfig
|
|
894
|
+
);
|
|
895
|
+
expect(fetchData).toHaveBeenCalledTimes(1);
|
|
896
|
+
expect(editItem).toHaveBeenCalledTimes(0);
|
|
897
|
+
|
|
898
|
+
await user.click(screen.getByTestId("ef-save"));
|
|
899
|
+
expect(editItem).toHaveBeenCalledTimes(1);
|
|
900
|
+
await waitFor(() => expect(fetchData).toHaveBeenCalledTimes(2));
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("does not fetch on mount if a fetch is already in progress", () => {
|
|
904
|
+
renderWithContext(
|
|
905
|
+
<ThingEditableConfigList {...baseProps({ isFetching: true })} />,
|
|
906
|
+
systemConfig
|
|
907
|
+
);
|
|
908
|
+
expect(fetchData).toHaveBeenCalledTimes(0);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it("navigates to the edit page after successfully creating a limit-one item", async () => {
|
|
912
|
+
const user = userEvent.setup();
|
|
913
|
+
const navigateSpy = jest
|
|
914
|
+
.spyOn(navigate, "navigateTo")
|
|
915
|
+
.mockImplementation(() => undefined);
|
|
916
|
+
renderWithContext(
|
|
917
|
+
<OneThingEditableConfigList
|
|
918
|
+
{...baseProps({ editOrCreate: "create", responseBody: "99" })}
|
|
919
|
+
/>,
|
|
920
|
+
systemConfig
|
|
921
|
+
);
|
|
922
|
+
await user.click(screen.getByTestId("ef-save"));
|
|
923
|
+
await waitFor(
|
|
924
|
+
() => expect(navigateSpy).toHaveBeenCalledWith("/admin/things/edit/99"),
|
|
925
|
+
{ timeout: 3000 }
|
|
926
|
+
);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it("does not render AdditionalContent unless the subclass supplies it", () => {
|
|
930
|
+
const { container, unmount } = renderWithContext(
|
|
931
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
932
|
+
systemConfig
|
|
933
|
+
);
|
|
934
|
+
expect(container.querySelector(".additional-content")).toBeNull();
|
|
935
|
+
unmount();
|
|
936
|
+
|
|
937
|
+
const withContent = renderWithContext(
|
|
938
|
+
<ThingAdditionalContentEditableConfigList {...baseProps()} />,
|
|
939
|
+
systemConfig
|
|
940
|
+
);
|
|
941
|
+
const additional = withContent.container.querySelector<HTMLElement>(
|
|
942
|
+
".additional-content"
|
|
943
|
+
);
|
|
944
|
+
expect(additional).not.toBeNull();
|
|
945
|
+
expect(additional.textContent).toBe("Test Additional Content");
|
|
946
|
+
expect(additional.getAttribute("data-item-id")).toBe("5");
|
|
947
|
+
expect(additional.getAttribute("data-csrf")).toBe("token");
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("shows the troubleshooting link and info alert only when there are self-tests", () => {
|
|
951
|
+
const { container, unmount } = renderWithContext(
|
|
952
|
+
<ThingEditableConfigList {...baseProps()} />,
|
|
953
|
+
systemConfig
|
|
954
|
+
);
|
|
955
|
+
// No self-tests configured → neither the info alert nor the footer link.
|
|
956
|
+
expect(container.querySelector(".alert-info")).toBeNull();
|
|
957
|
+
expect(screen.queryByText(/visit the troubleshooting page/)).toBeNull();
|
|
958
|
+
unmount();
|
|
959
|
+
|
|
960
|
+
const withSelfTests = renderWithContext(
|
|
961
|
+
<ThingWithSelfTests {...baseProps()} />,
|
|
962
|
+
systemConfig
|
|
963
|
+
);
|
|
964
|
+
const info = withSelfTests.container.querySelector(".alert-info");
|
|
965
|
+
expect(info.textContent).toBe(
|
|
966
|
+
"Self-tests for the things have been moved to the troubleshooting page."
|
|
967
|
+
);
|
|
968
|
+
expect(info.querySelector("a").getAttribute("href")).toBe(
|
|
969
|
+
"/admin/web/troubleshooting/self-tests/thingServices"
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const footer = withSelfTests.container.querySelector("p");
|
|
973
|
+
expect(footer.textContent).toBe(
|
|
974
|
+
"Problems with your things? Please visit the troubleshooting page."
|
|
975
|
+
);
|
|
976
|
+
expect(footer.querySelector("a").getAttribute("href")).toBe(
|
|
977
|
+
"/admin/web/troubleshooting/self-tests/thingServices"
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it("figures out what level of permissions the admin has", () => {
|
|
982
|
+
const adminLevelFor = (roles) => {
|
|
983
|
+
const { getByTestId, unmount } = renderWithContext(
|
|
984
|
+
<BaseThingList {...baseProps({ editOrCreate: "create" })} />,
|
|
985
|
+
configWithRoles(roles)
|
|
986
|
+
);
|
|
987
|
+
const level = getByTestId("ef-adminlevel").textContent;
|
|
988
|
+
unmount();
|
|
989
|
+
return level;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
expect(adminLevelFor([{ role: "system", library: "nypl" }])).toBe("3");
|
|
993
|
+
expect(adminLevelFor([{ role: "manager", library: "nypl" }])).toBe("2");
|
|
994
|
+
expect(adminLevelFor([{ role: "librarian", library: "nypl" }])).toBe("1");
|
|
995
|
+
});
|
|
996
|
+
});
|