From 42b4fe5e0fd6957147f41e704619e26b4360b706 Mon Sep 17 00:00:00 2001 From: Jonathan Green Date: Mon, 13 Jul 2026 10:00:43 -0300 Subject: [PATCH 1/3] Rewrite service and collection configuration test suites with React Testing Library --- src/components/__tests__/Collections-test.tsx | 223 ---- .../__tests__/EditableConfigList-test.tsx | 576 --------- .../__tests__/ProtocolFormField-test.tsx | 534 -------- src/components/__tests__/SelfTests-test.tsx | 449 ------- .../__tests__/ServiceEditForm-test.tsx | 1087 ---------------- tests/jest/components/Collections.test.tsx | 342 +++++- .../components/EditableConfigList.test.tsx | 605 ++++++++- .../components/ProtocolFormField.test.tsx | 450 ++++++- tests/jest/components/SelfTests.test.tsx | 601 +++++++++ .../jest/components/ServiceEditForm.test.tsx | 1092 +++++++++++++++++ 10 files changed, 3069 insertions(+), 2890 deletions(-) delete mode 100644 src/components/__tests__/Collections-test.tsx delete mode 100644 src/components/__tests__/EditableConfigList-test.tsx delete mode 100644 src/components/__tests__/ProtocolFormField-test.tsx delete mode 100644 src/components/__tests__/SelfTests-test.tsx delete mode 100644 src/components/__tests__/ServiceEditForm-test.tsx create mode 100644 tests/jest/components/SelfTests.test.tsx create mode 100644 tests/jest/components/ServiceEditForm.test.tsx diff --git a/src/components/__tests__/Collections-test.tsx b/src/components/__tests__/Collections-test.tsx deleted file mode 100644 index fff256719b..0000000000 --- a/src/components/__tests__/Collections-test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import { stub } from "sinon"; -import * as React from "react"; -import { ReactWrapper, mount } from "enzyme"; -import Admin from "../../models/Admin"; -import { Collections, CollectionEditForm } from "../Collections"; -import buildStore from "../../store"; - -const collections = [ - { - id: "2", - protocol: "test protocol", - marked_for_deletion: false, - name: "ODL", - }, - { - id: "3", - protocol: "test protocol", - marked_for_deletion: true, - name: "Enki", - }, - { - id: "4", - protocol: "test protocol", - marked_for_deletion: false, - name: "RBDigital", - }, -]; - -describe("Collections", () => { - let wrapper; - let registerLibrary; - let fetchLibraryRegistrations; - const systemAdmin = new Admin([{ role: "system", library: "nypl" }]); - - describe("In Edit mode", () => { - beforeEach(() => { - registerLibrary = stub().returns( - new Promise((resolve) => resolve()) - ); - fetchLibraryRegistrations = stub(); - wrapper = mount( - , - { context: { admin: systemAdmin } } - ); - }); - - it("includes registerLibrary in child context, and fetches library registrations on mount and after registering", async () => { - const context = wrapper.instance().getChildContext(); - - expect(fetchLibraryRegistrations.callCount).to.equal(1); - - const library = { short_name: "nypl" }; - context.registerLibrary(library); - - expect(registerLibrary.callCount).to.equal(1); - const formData = registerLibrary.args[0][0]; - expect(formData.get("library_short_name")).to.equal("nypl"); - expect(formData.get("collection_id")).to.equal("2"); - - const pause = new Promise((resolve) => setTimeout(resolve, 0)); - await pause; - expect(fetchLibraryRegistrations.callCount).to.equal(2); - }); - }); - - describe("In create/list mode", () => { - beforeEach(() => { - registerLibrary = stub().returns( - new Promise((resolve) => resolve()) - ); - fetchLibraryRegistrations = stub(); - const store = buildStore(); - wrapper = mount( - , - { context: { admin: systemAdmin } } - ); - }); - - it("should render a list of collections and the second collection is marked for deletion", () => { - const h2 = wrapper.find("h2"); - const ul = wrapper.find("ul"); - const li = ul.find("li"); - // The second collection is marked for deletion. - const deletedCollection = li.at(1); - - expect(h2.text()).to.equal("Collection configuration"); - expect(ul.length).to.equal(1); - expect(li.length).to.equal(3); - // Only one collection is marked as deleted and that list item - // should have the `deleted-collection` class for display in the UI - expect(ul.find(".deleted-collection").length).to.equal(1); - // The second collection is marked for deletion. - expect(deletedCollection.find("h4").text()).to.equal("Enki"); - expect(deletedCollection.find("p").text()).to.equal( - "This collection cannot be edited and is currently being deleted. " + - "The deletion process is gradual and this collection will be removed once it is complete." - ); - }); - - it("should not render edit or delete buttons for the deleted collection", () => { - const ul = wrapper.find("ul"); - const li = ul.find("li"); - const firstCollection = li.at(0); - const deletedCollection = li.at(1); - - expect(firstCollection.find("a.edit-item").length).to.equal(1); - expect(firstCollection.find("button.delete-item").length).to.equal(1); - expect(deletedCollection.find("a.edit-item").length).to.equal(0); - expect(deletedCollection.find("button.delete-item").length).to.equal(0); - }); - - describe("confirm before disassociating libraries", () => { - let wrapper: ReactWrapper; - let confirmStub: sinon.SinonStub; - let instance: CollectionEditForm; - - const initialLibraries = [ - { short_name: "palace", name: "Palace" }, - { short_name: "another-library", name: "Another Library" }, - ] as const; - const collection = { - id: 7, - name: "An OPDS Collection", - protocol: "OPDS Import", - libraries: [...initialLibraries], - }; - - beforeEach(() => { - confirmStub = sinon.stub(window, "confirm"); - wrapper = mount( - - ); - instance = wrapper.instance() as CollectionEditForm; - }); - - afterEach(() => { - confirmStub.restore(); - }); - - it("prompts for confirmation before removing a library", () => { - confirmStub.returns(false); - - // The confirmation dialog should not be invoked before we click. - expect(confirmStub.called).to.be.false; - - wrapper.find("button.remove-btn").at(0).simulate("click"); - expect(confirmStub.calledOnce).to.be.true; - const message: string = confirmStub.firstCall.args[0]; - expect(message).to.equal( - 'Disassociating library "Palace" from this collection will ' + - "remove all loans and holds for its patrons. Do you wish to continue?" - ); - }); - - it("removes library if confirmation is accepted", () => { - confirmStub.returns(true); - wrapper.find("button.remove-btn").at(0).simulate("click"); - - // Ensure confirmation was sought. - expect(confirmStub.calledOnce).to.be.true; - - // We deleted the first library, so it should be gone from the state. - expect(wrapper.state("libraries")).to.deep.equal([initialLibraries[1]]); - }); - - it("does not remove library if confirmation is canceled", () => { - confirmStub.returns(false); - wrapper.find("button.remove-btn").at(0).simulate("click"); - - // Ensure confirmation was sought. - expect(confirmStub.calledOnce).to.be.true; - - // We didn't delete, so we should still have the originals. - expect(wrapper.state("libraries")).to.deep.equal(initialLibraries); - }); - - it("uses library short_name in confirmation when full name is not available", () => { - const getLibraryStub = stub(instance, "getLibrary").returns(null); - confirmStub.returns(false); - - wrapper.find("button.remove-btn").at(0).simulate("click"); - - expect(confirmStub.calledOnce).to.be.true; - const message: string = confirmStub.firstCall.args[0]; - const libraryShortName = initialLibraries[0].short_name; - expect(message).to.equal( - `Disassociating library "${libraryShortName}" from this collection will ` + - "remove all loans and holds for its patrons. Do you wish to continue?" - ); - getLibraryStub.restore(); - }); - }); - }); -}); diff --git a/src/components/__tests__/EditableConfigList-test.tsx b/src/components/__tests__/EditableConfigList-test.tsx deleted file mode 100644 index dc28ea67ad..0000000000 --- a/src/components/__tests__/EditableConfigList-test.tsx +++ /dev/null @@ -1,576 +0,0 @@ -import { expect } from "chai"; -import { stub } from "sinon"; - -import * as React from "react"; -import { shallow, mount } from "enzyme"; - -import Admin from "../../models/Admin"; -import { - EditableConfigList, - EditFormProps, - AdditionalContentProps, -} from "../EditableConfigList"; -import ErrorMessage from "../ErrorMessage"; -import { Alert } from "react-bootstrap"; -import LoadingIndicator from "@thepalaceproject/web-opds-client/lib/components/LoadingIndicator"; - -describe("EditableConfigList", () => { - interface Thing { - id: number; - label: string; - } - - interface Things { - things: Thing[]; - } - - class ThingEditForm extends React.Component< - EditFormProps, - {} - > { - render(): JSX.Element { - return
Test
; - } - } - - class AdditionalContent extends React.Component< - AdditionalContentProps, - {} - > { - render(): JSX.Element { - return
Test Additional Content
; - } - } - - let canCreate: boolean; - let canDelete: boolean; - let canEdit: boolean; - - class ThingEditableConfigList extends EditableConfigList { - EditForm = ThingEditForm; - listDataKey = "things"; - itemTypeName = "thing"; - urlBase = "/admin/things/"; - identifierKey = "id"; - labelKey = "label"; - - label(item): string { - return "test " + super.label(item); - } - - canCreate() { - return canCreate; - } - - canDelete() { - return canDelete; - } - - canEdit() { - return canEdit; - } - } - - class OneThingEditableConfigList extends ThingEditableConfigList { - limitOne = true; - } - - class ThingAdditionalContentEditableConfigList extends ThingEditableConfigList { - AdditionalContent = AdditionalContent; - } - - class ThingWithSelfTests extends ThingEditableConfigList { - links = { - info: ( - <> - Self-tests for the things have been moved to{" "} - - the troubleshooting page - - . - - ), - footer: ( - <> - Problems with your things? Please visit{" "} - - the troubleshooting page - - . - - ), - }; - } - - let wrapper; - let fetchData; - let editItem; - let deleteItem; - const thingData = { id: 5, label: "label" }; - const thingsData = { things: [thingData] }; - - const pause = () => { - return new Promise((resolve) => setTimeout(resolve, 0)); - }; - - const systemAdmin = new Admin([{ role: "system", library: "nypl" }]); - const libraryManager = new Admin([{ role: "manager", library: "nypl" }]); - - beforeEach(() => { - fetchData = stub(); - editItem = stub().returns( - new Promise((resolve) => resolve()) - ); - deleteItem = stub().returns( - new Promise((resolve) => resolve()) - ); - canCreate = true; - canDelete = true; - canEdit = true; - - wrapper = mount( - , - { context: { admin: systemAdmin } } - ); - }); - - it("shows an error message if there's a problem loading the list", () => { - let error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(0); - - const fetchError = { - status: 404, - response: "test load error", - url: "test url", - }; - wrapper.setProps({ fetchError }); - - error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(1); - expect(error.text()).to.equal("Error: test load error"); - - // Hide the error message when the user goes to the form - wrapper.setProps({ editOrCreate: "create" }); - error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(0); - }); - - it("shows form submission error message only if the form is displayed", () => { - let error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(0); - - const formError = { - status: 400, - response: "test submission error", - url: "test url", - }; - wrapper.setProps({ formError }); - error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(0); - - wrapper.setProps({ editOrCreate: "create" }); - error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(1); - expect(error.text()).to.equal("Error: test submission error"); - - // Hide the error message when the user goes back to the list - wrapper.setProps({ editOrCreate: "" }); - error = wrapper.find(ErrorMessage); - expect(error.length).to.equal(0); - }); - - it("shows success message on create", () => { - let success = wrapper.find(Alert); - expect(success.length).to.equal(0); - wrapper.setProps({ responseBody: "itemType", editOrCreate: "create" }); - success = wrapper.find(Alert); - expect(success.length).to.equal(1); - expect(success.text()).to.equal("Successfully created a new thing"); - }); - - it("displays edit link in success message on create", () => { - wrapper.setProps({ responseBody: "itemType", editOrCreate: "create" }); - const success = wrapper.find(Alert); - const link = success.find("a"); - expect(link.length).to.equal(1); - expect(link.props().href).to.equal("/admin/things/edit/itemType"); - }); - - it("shows success message on edit", () => { - let success = wrapper.find(Alert); - expect(success.length).to.equal(0); - wrapper.setProps({ responseBody: "itemType", editOrCreate: "edit" }); - success = wrapper.find(Alert); - expect(success.length).to.equal(1); - expect(success.text()).to.equal("Successfully edited this thing"); - }); - - it("does not display edit link in success message on edit", () => { - wrapper.setProps({ responseBody: "itemType", editOrCreate: "edit" }); - const success = wrapper.find(Alert); - const link = success.find("a"); - expect(link.length).to.equal(0); - }); - - it("correctly formats item type name for success message", () => { - expect(wrapper.instance().formatItemType()).to.equal("thing"); - const getItemType = stub(wrapper.instance(), "getItemType").returns( - "ALLCAPS service" - ); - expect(wrapper.instance().formatItemType()).to.equal("ALLCAPS service"); - getItemType.returns("someCAPS"); - expect(wrapper.instance().formatItemType()).to.equal("somecaps"); - getItemType.restore(); - }); - - it("shows loading indicator", () => { - let loading = wrapper.find(LoadingIndicator); - expect(loading.length).to.equal(0); - wrapper.setProps({ isFetching: true }); - loading = wrapper.find(LoadingIndicator); - expect(loading.length).to.equal(1); - }); - - it("shows thing header", () => { - const header = wrapper.find("h2"); - expect(header.text()).to.equal("Thing configuration"); - }); - - it("shows thing list", () => { - const things = wrapper.find("li"); - expect(things.length).to.equal(1); - expect(things.at(0).text()).to.contain("test label"); - const editLink = things.at(0).find(".edit-item").at(0); - expect(editLink.prop("href")).to.equal("/admin/things/edit/5"); - }); - - it("shows the count of things in the list", () => { - const things = wrapper.find(".list-container header div"); - expect(things.length).to.equal(1); - expect(things.at(0).text()).to.equal("1 configured"); - }); - - it("updates thing list", () => { - const newThing = { id: 6, label: "another thing" }; - const newThingsData = { things: [thingData, newThing] }; - wrapper.setProps({ data: newThingsData }); - - const things = wrapper.find("li"); - expect(things.length).to.equal(2); - expect(things.at(1).text()).to.contain("test another thing"); - const editLink = things.at(1).find(".edit-item").at(0); - expect(editLink.props().href).to.equal("/admin/things/edit/6"); - }); - - it("shows create link", () => { - const createLink = wrapper.find(".create-item").at(0); - expect(createLink.text()).to.equal("Create new thing"); - expect(createLink.props().href).to.equal("/admin/things/create"); - }); - - it("hides create link if canCreate returns false", () => { - canCreate = false; - wrapper = shallow( - - ); - const createLink = wrapper.find(".create-item"); - expect(createLink.length).to.equal(0); - }); - - it("hides create link if only one item is allowed and it already exists", () => { - wrapper = shallow( - - ); - let createLink = wrapper - .find(".create-item") - .findWhere((el) => el.text().includes("Create new")); - expect(createLink.length).to.equal(0); - - wrapper = mount( - - ); - createLink = wrapper.find(".create-item"); - expect(createLink.length).to.equal(1); - expect(createLink.text()).to.equal("Create new thing"); - expect(createLink.prop("href")).to.equal("/admin/things/create"); - }); - - it("displays a view button instead of an edit button if canEdit returns false", () => { - canEdit = false; - const viewableThing = { id: 6, label: "View Only", level: 3 }; - wrapper = shallow( - , - { context: { admin: libraryManager } } - ); - expect(wrapper.instance().canEdit(viewableThing)).to.be.false; - const viewLink = wrapper.find("span"); - expect(viewLink.text()).to.contain("View"); - expect(viewLink.text()).not.to.contain("Edit"); - }); - - it("hides delete button if canDelete returns false", () => { - canDelete = false; - wrapper = shallow( - - ); - - const things = wrapper.find("li"); - expect(things.length).to.equal(1); - const deleteButton = things.at(0).find(".delete-item"); - expect(deleteButton.length).to.equal(0); - }); - - it("deletes an item", () => { - const confirmStub = stub(window, "confirm").returns(false); - - const things = wrapper.find("li"); - expect(things.length).to.equal(1); - const deleteButton = things.at(0).find(".delete-item").hostNodes(); - expect(deleteButton.length).to.equal(1); - deleteButton.simulate("click"); - - expect(deleteItem.callCount).to.equal(0); - - confirmStub.returns(true); - deleteButton.simulate("click"); - - expect(deleteItem.callCount).to.equal(1); - expect(deleteItem.args[0][0]).to.equal(5); - - confirmStub.restore(); - }); - - it("shows create form", () => { - let form = wrapper.find(ThingEditForm); - expect(form.length).to.equal(0); - wrapper.setProps({ editOrCreate: "create" }); - form = wrapper.find(ThingEditForm); - expect(form.length).to.equal(1); - expect(form.props().data).to.deep.equal(thingsData); - expect(form.props().item).to.be.undefined; - expect(form.props().disabled).to.equal(false); - expect(form.props().listDataKey).to.equal("things"); - }); - - it("shows correct header on create form", () => { - wrapper.setProps({ editOrCreate: "create" }); - const formHeader = wrapper.find("h3"); - expect(formHeader.length).to.equal(1); - expect(formHeader.text()).to.equal("Create a new thing"); - }); - - it("shows edit form", () => { - wrapper.setProps({ editOrCreate: "edit", identifier: "5" }); - const form = wrapper.find(ThingEditForm); - expect(form.length).to.equal(1); - expect(form.props().data).to.deep.equal(thingsData); - expect(form.props().item).to.equal(thingData); - expect(form.props().disabled).to.equal(false); - expect(form.props().listDataKey).to.equal("things"); - }); - - it("shows correct header on edit form for an item that can be edited", () => { - wrapper.setProps({ editOrCreate: "edit", identifier: "5" }); - const formHeader = wrapper.find("h3"); - expect(formHeader.length).to.equal(1); - expect(formHeader.text()).to.equal("Edit test label"); - }); - - it("shows correct header on edit form for an item that can not be edited", () => { - canEdit = false; - wrapper.setProps({ editOrCreate: "edit", identifier: "5" }); - const formHeader = wrapper.find("h3"); - expect(formHeader.length).to.equal(1); - expect(formHeader.text()).to.equal("test label"); - }); - - it("updates header on edit form", () => { - wrapper.setProps({ editOrCreate: "edit", identifier: "5" }); - const formHeader = wrapper.find("h3"); - expect(formHeader.text()).to.equal("Edit test label"); - - const newThingData = { id: 5, label: "new thing!" }; - const newThingsData = { things: [newThingData] }; - wrapper.setProps({ data: newThingsData }); - - expect(formHeader.text()).to.equal("Edit test new thing!"); - }); - - it("does not supply a save function to the edit form if canEdit returns false", () => { - canEdit = false; - wrapper.setProps({ editOrCreate: "edit", identifier: "5" }); - const editForm = wrapper.find(ThingEditForm); - expect(editForm.props().save).to.equal(undefined); - }); - - it("disables the edit form if canEdit returns false", () => { - canEdit = false; - wrapper.setProps({ editOrCreate: "edit", identifier: "5" }); - const editForm = wrapper.find(ThingEditForm); - expect(editForm.props().disabled).to.equal(true); - }); - - it("fetches data on mount and passes save function to form", () => { - expect(fetchData.callCount).to.equal(1); - - wrapper.setProps({ editOrCreate: "create" }); - const form = wrapper.find(ThingEditForm); - - expect(editItem.callCount).to.equal(0); - form.props().save(); - expect(editItem.callCount).to.equal(1); - }); - - it("does not fetch data on mount if a fetch is already in progress", () => { - // Expect one pre-existing call to fetchData from the component mounted in beforeEach()... - expect(fetchData.callCount).to.equal(1); - - wrapper = shallow( - , - { context: { admin: systemAdmin } } - ); - - // ...but no more! - expect(fetchData.callCount).to.equal(1); - }); - - it("fetches data again on save", async () => { - wrapper.setProps({ editOrCreate: "create" }); - const form = wrapper.find(ThingEditForm); - form.props().save(); - await pause(); - expect(fetchData.callCount).to.equal(2); - }); - - it("should not render the AdditionalContent component", () => { - const additionalContent = wrapper.find(AdditionalContent); - expect(additionalContent.length).to.equal(0); - }); - - it("should render the AdditionalContent component", () => { - wrapper = shallow( - - ); - - const additionalContent = wrapper.find(AdditionalContent); - expect(additionalContent.length).to.equal(1); - expect(additionalContent.props().item).to.deep.equal(thingData); - expect(additionalContent.props().csrfToken).to.equal("token"); - }); - - it("should not render a troubleshooting link if there are no self-tests", () => { - const link = wrapper.find("h5"); - expect(link.length).to.equal(0); - }); - - it("should not render an info alert if there are no self-tests", () => { - const alert = wrapper.find(Alert); - expect(alert.length).to.equal(0); - }); - - it("should render a troubleshooting link if there are self-tests", () => { - wrapper = shallow( - - ); - const link = wrapper.find("p"); - expect(link.length).to.equal(1); - expect(link.text()).to.equal( - "Problems with your things? Please visit the troubleshooting page." - ); - expect(link.find("a").prop("href")).to.equal( - "/admin/web/troubleshooting/self-tests/thingServices" - ); - }); - - it("should render an info alert if there are self-tests", () => { - wrapper = mount( - - ); - const alert = wrapper.find(".alert"); - expect(alert.length).to.equal(1); - expect(alert.text()).to.equal( - "Self-tests for the things have been moved to the troubleshooting page." - ); - expect(alert.find("a").prop("href")).to.equal( - "/admin/web/troubleshooting/self-tests/thingServices" - ); - }); - - it("figures out what level of permissions the admin has", () => { - const libraryManager = new Admin([{ role: "manager", library: "nypl" }]); - const librarian = new Admin([{ role: "librarian", library: "nypl" }]); - expect(wrapper.instance().getAdminLevel()).to.equal(3); - wrapper.setContext({ admin: libraryManager }); - expect(wrapper.instance().getAdminLevel()).to.equal(2); - wrapper.setContext({ admin: librarian }); - expect(wrapper.instance().getAdminLevel()).to.equal(1); - }); -}); diff --git a/src/components/__tests__/ProtocolFormField-test.tsx b/src/components/__tests__/ProtocolFormField-test.tsx deleted file mode 100644 index 3bc44e0145..0000000000 --- a/src/components/__tests__/ProtocolFormField-test.tsx +++ /dev/null @@ -1,534 +0,0 @@ -import { expect } from "chai"; - -import * as React from "react"; -import { mount, ReactWrapper } from "enzyme"; -import { stub } from "sinon"; - -import ProtocolFormField from "../ProtocolFormField"; -import EditableInput from "../EditableInput"; -import InputList from "../InputList"; -import ColorPicker from "../ColorPicker"; -import { Button } from "library-simplified-reusable-components"; - -describe("ProtocolFormField", () => { - describe("When visible...", () => { - const setting = { - key: "setting", - label: "label", - description: "

description

", - }; - let wrapper; - - beforeEach(() => { - wrapper = mount(); - }); - - it("renders text setting", () => { - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("type")).to.equal("text"); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("description")).to.equal("

description

"); - expect(input.prop("value")).to.be.undefined; - - wrapper.setProps({ value: "test", disabled: true }); - input = wrapper.find(EditableInput); - expect(input.prop("disabled")).to.equal(true); - expect(input.prop("value")).to.equal("test"); - const inputElement = input.find("input").getDOMNode(); - expect(inputElement.value).to.equal("test"); - - (wrapper.instance() as ProtocolFormField).clear(); - expect(inputElement.value).to.equal(""); - }); - - it("renders date-picker setting", () => { - const datePickerSetting = { ...setting, ...{ type: "date-picker" } }; - wrapper.setProps({ setting: datePickerSetting }); - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("type")).to.equal("date"); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("description")).to.equal("

description

"); - expect(input.prop("value")).to.be.undefined; - - wrapper.setProps({ value: "2020-10-13", disabled: true }); - input = wrapper.find(EditableInput); - expect(input.prop("disabled")).to.equal(true); - expect(input.prop("value")).to.equal("2020-10-13"); - const inputElement = input.find("input").getDOMNode(); - expect(inputElement.value).to.equal("2020-10-13"); - - (wrapper.instance() as ProtocolFormField).clear(); - expect(inputElement.value).to.equal(""); - }); - - it("renders optional setting", () => { - const optionalSetting = { ...setting, ...{ optional: true } }; - wrapper.setProps({ setting, optionalSetting }); - - const input = wrapper.find(EditableInput); - const description = wrapper.find(".description"); - expect(input.length).to.equal(1); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(description.text()).to.equal("(Optional) description"); - }); - - it("renders randomizable setting", () => { - const randomizableSetting = { ...setting, ...{ randomizable: true } }; - wrapper.setProps({ setting: randomizableSetting }); - - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("value")).to.be.undefined; - let button = wrapper.find(Button); - expect(button.length).to.equal(1); - expect(button.prop("disabled")).to.equal(false); - expect(button.text()).to.contain("random"); - - button.simulate("click"); - expect((wrapper.instance() as ProtocolFormField).getValue()).to.be.ok; - expect( - (wrapper.instance() as ProtocolFormField).getValue().length - ).to.equal(32); - - wrapper.setProps({ value: "test" }); - input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("value")).to.equal("test"); - button = wrapper.find("button"); - expect(button.length).to.equal(0); - }); - - it("renders setting with default", () => { - const defaultSetting = { ...setting, ...{ default: "default" } }; - wrapper.setProps({ setting: defaultSetting }); - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("value")).to.equal("default"); - - wrapper.setProps({ value: "test" }); - input = wrapper.find(EditableInput); - expect(input.prop("value")).to.equal("test"); - }); - - it("renders number setting", () => { - const numberSetting = { ...setting, ...{ type: "number" } }; - wrapper.setProps({ setting: numberSetting }); - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("validation")).to.equal("number"); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("description")).to.equal("

description

"); - expect(input.prop("value")).to.be.undefined; - - wrapper.setProps({ value: "test" }); - input = wrapper.find(EditableInput); - expect(input.prop("value")).to.equal("test"); - }); - - it("renders select setting", () => { - const selectSetting = { - ...setting, - ...{ - type: "select", - options: [ - { key: "option1", label: "option 1" }, - { key: "option2", label: "option 2" }, - ], - }, - }; - wrapper.setProps({ setting: selectSetting }); - - const input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("value")).to.be.undefined; - const children = input.find("option"); - expect(children.length).to.equal(2); - expect(children.at(0).prop("value")).to.equal("option1"); - expect(children.at(0).text()).to.contain("option 1"); - expect(children.at(1).prop("value")).to.equal("option2"); - expect(children.at(1).text()).to.contain("option 2"); - }); - - it("renders textarea setting", () => { - const textareaSetting = { - ...setting, - ...{ type: "textarea", description: "

Textarea

" }, - }; - wrapper.setProps({ setting: textareaSetting }); - - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - const inputElement = input.find("textarea").at(0) as any; - expect(inputElement.length).to.equal(1); - expect(inputElement.text()).to.equal(""); - expect(input.prop("type")).to.equal("text"); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("description")).to.equal("

Textarea

"); - expect(input.prop("value")).to.be.undefined; - - wrapper.setProps({ value: "test" }); - input = wrapper.find(EditableInput); - expect(input.prop("value")).to.equal("test"); - expect(inputElement.text()).to.equal("test"); - - (wrapper.instance() as ProtocolFormField).clear(); - expect(inputElement.text()).to.equal(""); - }); - - it("renders menu setting", () => { - let inputList = wrapper.find(InputList); - expect(inputList.length).to.equal(0); - const menuSetting = { - ...setting, - ...{ - type: "menu", - menuOptions: ["A", "B", "C"].map((x) => ( - - )), - }, - }; - wrapper.setProps({ - setting: menuSetting, - value: [], - altValue: "Alternate", - readOnly: true, - disableButton: true, - }); - inputList = wrapper.find(InputList); - expect(inputList.length).to.equal(1); - expect(inputList.prop("setting")).to.equal(menuSetting); - expect(inputList.prop("altValue")).to.equal("Alternate"); - expect(inputList.find("select").length).to.equal(1); - expect(inputList.prop("readOnly")).to.be.true; - expect(inputList.prop("disableButton")).to.be.true; - }); - - it("renders image setting", () => { - const imageSetting = { ...setting, ...{ type: "image" } }; - wrapper.setProps({ setting: imageSetting }); - - let input = wrapper.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("type")).to.equal("file"); - expect(input.prop("disabled")).to.equal(false); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("description")).to.equal("

description

"); - expect(input.prop("value")).to.be.undefined; - expect(input.prop("accept")).to.equal("image/*"); - let label = wrapper.find("label"); - expect(label.text()).to.equal("label"); - - wrapper.setProps({ value: "image data" }); - input = wrapper.find(EditableInput); - expect(input.prop("value")).to.be.undefined; - label = wrapper.find("label"); - expect(label.text()).to.equal("label"); - const img = wrapper.find("img"); - expect(img.prop("src")).to.equal("image data"); - }); - - it("renders color picker setting", () => { - const colorPickerSetting = { - ...setting, - ...{ type: "color-picker", default: "#aaaaaa" }, - }; - wrapper.setProps({ setting: colorPickerSetting }); - - let picker = wrapper.find(ColorPicker); - expect(picker.length).to.equal(1); - expect(picker.prop("setting")).to.equal(colorPickerSetting); - expect(picker.prop("value")).to.equal("#aaaaaa"); - const label = wrapper.find("label").at(0); - expect(label.text()).to.equal("label"); - - wrapper.setProps({ value: "#222222" }); - picker = wrapper.find(ColorPicker); - expect(picker.prop("value")).to.equal("#222222"); - }); - - it("gets value of list setting without options", () => { - wrapper.setProps({ - setting: { ...setting, ...{ type: "list" } }, - value: ["item 1", "item 2"], - }); - expect( - (wrapper.instance() as ProtocolFormField).getValue() - ).to.deep.equal(["item 1", "item 2"]); - }); - - it("optionally renders instructions", () => { - const instructionsSetting = { - ...setting, - ...{ instructions: "
  • Step 1
", type: "list" }, - }; - wrapper.setProps({ setting: instructionsSetting }); - - const instructions = wrapper.find(".well"); - expect(instructions.length).to.equal(1); - expect(instructions.hasClass("description")).to.be.true; - expect(instructions.text()).to.equal("Step 1"); - }); - - it("optionally accepts an onChange prop", () => { - const onChange = stub(); - wrapper.setProps({ onChange }); - const element = wrapper.find(EditableInput); - expect(element.prop("onChange")).to.equal(onChange); - const setting = { ...wrapper.prop("setting"), ...{ type: "list" } }; - wrapper.setProps({ setting }); - const inputList = wrapper.find(InputList); - expect(inputList.prop("onChange")).to.equal(onChange); - }); - - it("optionally accepts a readOnly prop", () => { - const setting = { ...wrapper.prop("setting"), ...{ type: "list" } }; - wrapper.setProps({ setting: setting, readOnly: true }); - const inputList = wrapper.find(InputList); - expect(inputList.prop("readOnly")).to.be.true; - }); - - it("gets value of text setting", () => { - wrapper.setProps({ value: "test" }); - expect((wrapper.instance() as ProtocolFormField).getValue()).to.equal( - "test" - ); - }); - }); - - describe("When hidden...", () => { - // For the hidden settings, the values are passed in the `settings` object. - const setting = { - hidden: true, - key: "setting", - label: "label", - description: "

description

", - }; - let wrapper: ReactWrapper; - - const expectHiddenValue = (value: any): ReactWrapper => { - const hiddenDiv = wrapper.findWhere( - (node) => node.type() === "div" && node.prop("style") !== undefined - ); - - expect( - (wrapper.instance() as ProtocolFormField).getValue() - ).to.deep.equal(value); - return hiddenDiv; - }; - - beforeEach(() => { - wrapper = mount(); - }); - - it("renders hidden text setting", () => { - wrapper.setProps({ value: "test" }); - - const hiddenDiv = expectHiddenValue("test"); - const input = hiddenDiv.find(EditableInput); - const inputElement = input.find("input").getDOMNode(); - - expect(input.prop("value")).to.equal("test"); - expect(inputElement.value).to.equal("test"); - expect(wrapper.instance().getValue()).to.equal("test"); - }); - - it("renders hidden date-picker setting", () => { - const datePickerSetting = { ...setting, type: "date-picker" }; - wrapper.setProps({ setting: datePickerSetting, value: "2020-10-13" }); - - expectHiddenValue("2020-10-13"); - }); - - it("renders hidden randomizable setting", () => { - const randomizableSetting = { ...setting, randomizable: true }; - wrapper.setProps({ setting: randomizableSetting, value: "test" }); - - expectHiddenValue("test"); - }); - - it("renders hidden setting with default", () => { - const defaultSetting = { ...setting, default: "default" }; - wrapper.setProps({ setting: defaultSetting, value: undefined }); - - expectHiddenValue("default"); - }); - - it("renders hidden number setting", () => { - const numberSetting = { ...setting, type: "number" }; - wrapper.setProps({ setting: numberSetting }); - - let hiddenDiv = expectHiddenValue(""); - - let input = hiddenDiv.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("validation")).to.equal("number"); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("value")).to.be.undefined; - - wrapper.setProps({ setting: numberSetting, value: "42" }); - hiddenDiv = expectHiddenValue("42"); - input = hiddenDiv.find(EditableInput); - expect(input.prop("value")).to.equal("42"); - }); - - it("renders hidden select setting", () => { - const selectSetting = { - ...setting, - type: "select", - options: [ - { key: "option1", label: "option 1" }, - { key: "option2", label: "option 2" }, - { key: "option3", label: "option 3" }, - ], - }; - - // If there is no value and no default, then we use the first option. - wrapper.setProps({ - setting: { ...selectSetting, default: undefined }, - value: undefined, - }); - const hiddenDiv = expectHiddenValue("option1"); - - const input = hiddenDiv.find(EditableInput); - const children = input.find("option"); - - expect(input.length).to.equal(1); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("value")).to.be.undefined; - - expect(children.length).to.equal(3); - expect(children.at(0).prop("value")).to.equal("option1"); - expect(children.at(1).prop("value")).to.equal("option2"); - expect(children.at(2).prop("value")).to.equal("option3"); - expect(children.at(0).text()).to.contain("option 1"); - expect(children.at(1).text()).to.contain("option 2"); - expect(children.at(2).text()).to.contain("option 3"); - - // If no value set and there is a default value, then we should get the default back. - wrapper.setProps({ - setting: { ...selectSetting, default: "option3" }, - value: undefined, - }); - expectHiddenValue("option3"); - - // If a value is already set, then we should get that value. - wrapper.setProps({ value: "option2" }); - expectHiddenValue("option2"); - }); - - it("renders hidden textarea setting", () => { - const textareaSetting = { ...setting, type: "textarea" }; - wrapper.setProps({ setting: textareaSetting }); - const hiddenDiv = expectHiddenValue(""); - - const input = hiddenDiv.find(EditableInput); - expect(input.length).to.equal(1); - const inputElement = input.find("textarea").at(0) as any; - expect(inputElement.length).to.equal(1); - expect(inputElement.text()).to.equal(""); - expect(input.prop("type")).to.equal("text"); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("value")).to.be.undefined; - - wrapper.setProps({ setting: textareaSetting, value: "test" }); - expectHiddenValue("test"); - }); - - it("renders hidden menu setting", () => { - const menuSetting = { - ...setting, - type: "menu", - menuOptions: ["A", "B", "C"].map((x) => ( - - )), - }; - wrapper.setProps({ - setting: menuSetting, - value: [], - altValue: "Alternate", - readOnly: true, - disableButton: true, - }); - - const hiddenDiv = expectHiddenValue([]); - const inputList = hiddenDiv.find(InputList); - expect(inputList.length).to.equal(1); - expect(inputList.prop("setting")).to.equal(menuSetting); - expect(inputList.prop("altValue")).to.equal("Alternate"); - expect(inputList.find("select").length).to.equal(1); - expect(inputList.prop("readOnly")).to.be.true; - expect(inputList.prop("disableButton")).to.be.true; - }); - - it("renders hidden image setting", () => { - const imageSetting = { ...setting, type: "image" }; - - wrapper.setProps({ setting: imageSetting }); - let hiddenDiv = expectHiddenValue(""); - - const input = hiddenDiv.find(EditableInput); - expect(input.length).to.equal(1); - expect(input.prop("type")).to.equal("file"); - expect(input.prop("name")).to.equal("setting"); - expect(input.prop("label")).to.equal("label"); - expect(input.prop("value")).to.be.undefined; - expect(input.prop("accept")).to.equal("image/*"); - - wrapper.setProps({ value: "data:image/png;base64,..." }); - hiddenDiv = expectHiddenValue(""); - const img = hiddenDiv.find("img"); - expect(img.prop("src")).to.equal("data:image/png;base64,..."); - }); - - it("renders hidden color picker setting", () => { - const colorPickerSetting = { - ...setting, - type: "color-picker", - default: "#aaaaaa", - }; - // If there is no value, then we use the default. - wrapper.setProps({ setting: colorPickerSetting, value: undefined }); - const hiddenDiv = expectHiddenValue("#aaaaaa"); - const picker = hiddenDiv.find(ColorPicker); - expect(picker.length).to.equal(1); - expect(picker.prop("setting")).to.equal(colorPickerSetting); - expect(picker.prop("value")).to.equal("#aaaaaa"); - - // If a value is provided, then we use that value. - wrapper.setProps({ value: "#222222" }); - expectHiddenValue("#aaaaaa"); - }); - - it("gets value of hidden list setting without options", () => { - wrapper.setProps({ - setting: { ...setting, type: "list" }, - value: ["item 1", "item 2"], - }); - expectHiddenValue(["item 1", "item 2"]); - }); - }); -}); diff --git a/src/components/__tests__/SelfTests-test.tsx b/src/components/__tests__/SelfTests-test.tsx deleted file mode 100644 index b7cc47355e..0000000000 --- a/src/components/__tests__/SelfTests-test.tsx +++ /dev/null @@ -1,449 +0,0 @@ -import { expect } from "chai"; -import { spy, stub } from "sinon"; - -import * as React from "react"; -import { shallow, mount } from "enzyme"; - -import { SelfTests } from "../SelfTests"; -import { CheckSoloIcon, XIcon } from "@nypl/dgx-svg-icons"; - -// SelfTests can take more than just a collection (an integration can have -// self tests), but just testing collection data right now. -const collections = [ - { - id: 1, - name: "collection 1", - protocol: "protocol", - libraries: [{ short_name: "library" }], - settings: { - external_account_id: "nypl", - }, - self_test_results: { - duration: 1.75, - start: "2018-08-07T19:34:54Z", - end: "2018-08-07T19:34:55Z", - results: [ - { - duration: 0.000119, - end: "2018-08-07T19:34:54Z", - exception: null, - name: "Initial setup.", - result: null, - start: "2018-08-07T19:34:54Z", - success: true, - }, - ], - }, - }, - { - id: 1, - name: "collection 1", - protocol: "protocol", - libraries: [{ short_name: "library" }], - settings: { - external_account_id: "nypl", - }, - self_test_results: { - duration: 1.75, - start: "2018-08-07T19:34:54Z", - end: "2018-08-07T19:34:55Z", - results: [ - { - duration: 0.000119, - end: "2018-08-07T19:34:54Z", - exception: null, - name: "Initial setup.", - result: null, - start: "2018-08-07T19:34:54Z", - success: true, - }, - { - duration: 0, - end: "2018-08-07T19:34:55Z", - exception: { - class: "IntegrationException", - debug_message: - "Add the collection to a library that has a patron authentication service.", - message: "Collection is not associated with any libraries.", - }, - name: "Acquiring test patron credentials.", - result: null, - start: "2018-08-07T19:34:55Z", - success: false, - }, - ], - }, - }, - { - id: 1, - name: "collection 1", - protocol: "protocol", - libraries: [{ short_name: "library" }], - settings: { - external_account_id: "nypl", - }, - self_test_results: { - exception: "Exception getting self-test results for collection ...", - duration: 0, - start: "", - end: "", - results: [], - }, - }, -]; - -const updatedCollection = { - id: 1, - name: "collection 1", - protocol: "protocol", - libraries: [{ short_name: "library" }], - settings: { - external_account_id: "nypl", - }, - self_test_results: { - duration: 1.75, - start: "2018-08-07T20:34:54Z", - end: "2018-08-07T20:34:55Z", - results: [ - { - duration: 0.000119, - end: "2018-08-07T20:34:54Z", - exception: null, - name: "Initial setup.", - result: null, - start: "2018-08-07T20:34:54Z", - success: true, - }, - ], - }, -}; - -describe("SelfTests", () => { - let wrapper; - - // Helper function to generate the expected date string - const getExpectedDate = (date: Date): string => { - return date.toDateString(); - }; - - // Helper function to generate the expected time string - const getExpectedTime = (date: Date): string => { - const hours = date.getHours().toString().padStart(2, "0"); - const minutes = date.getMinutes().toString().padStart(2, "0"); - const seconds = date.getSeconds().toString().padStart(2, "0"); - return `${hours}:${minutes}:${seconds}`; - }; - - beforeEach(() => { - wrapper = mount( - - ); - }); - - it("should render the SelfTests component with empty self_test_results", () => { - wrapper = shallow( - - ); - expect(wrapper.render().hasClass("integration-selftests")).to.equal(true); - expect(wrapper.find("ul").length).to.equal(0); - expect(wrapper.find("p").text()).to.equal("No self test results found."); - }); - - it("should disable the button if the tests cannot be run", () => { - const selfTestResultsWithException = { - ...collections[0].self_test_results, - ...{ exception: "Exception!", disabled: true }, - }; - const collectionWithException = { - ...collections[0], - ...{ self_test_results: selfTestResultsWithException }, - }; - wrapper = mount( - - ); - expect(wrapper.find("button").props().disabled).to.be.true; - }); - - it("should render the SelfTests component for new services", () => { - const exception = "This integration has no attribute 'prior_test_results'"; - const self_test_results = { - ...collections[0].self_test_results, - ...{ exception }, - }; - const item = { ...collections[0], ...{ self_test_results } }; - wrapper = shallow( - - ); - expect(wrapper.render().hasClass("integration-selftests")).to.equal(true); - expect(wrapper.find("ul").length).to.equal(0); - expect(wrapper.find(".description").text()).to.equal( - "There are no self test results yet." - ); - }); - - it("should render the SelfTests component with results", () => { - expect(wrapper.render().hasClass("integration-selftests")).to.equal(true); - expect(wrapper.find("ul").length).to.equal(1); - }); - - it("should format the date and duration of the most recent tests", () => { - const date = new Date(collections[0].self_test_results.start); - const expectedDate = getExpectedDate(date); - const expectedTime = getExpectedTime(date); - expect(wrapper.instance().formatDate(collections[0])).to.equal( - `Tests last ran on ${expectedDate} ${expectedTime} and lasted 1.75s.` - ); - }); - - it("should handle new props", () => { - expect(wrapper.state()["mostRecent"]).to.equal(wrapper.prop("item")); - wrapper.setProps({ item: updatedCollection }); - // The new item has a more recent start time, so the state gets updated. - expect(wrapper.state()["mostRecent"]).to.equal(updatedCollection); - wrapper.setProps({ item: collections[1] }); - // This is not a new result. Nothing happens. - expect(wrapper.state()["mostRecent"]).to.equal(updatedCollection); - }); - - describe("Successful self tests", () => { - it("should display information about the whole self test result", () => { - const passSVGIcon = wrapper.find(CheckSoloIcon); - const failSVGIcon = wrapper.find(XIcon); - const description = wrapper.find(".description"); - - // There's only one self test result in the collection and it passes. - expect(failSVGIcon.length).to.equal(0); - expect(passSVGIcon.length).to.equal(1); - - const date = new Date(collections[1].self_test_results.start); - const expectedDate = getExpectedDate(date); - const expectedTime = getExpectedTime(date); - expect(description.text().trim()).to.equal( - `Tests last ran on ${expectedDate} ${expectedTime} and lasted 1.75s.` - ); - }); - - it("should display detail information for each self test result for the collection", () => { - const list = wrapper.find("ul"); - const selfTestResults = list.find("li"); - expect(selfTestResults.length).to.equal(1); - expect(selfTestResults.hasClass("success")).to.be.true; - expect(selfTestResults.find("h4").text()).to.equal("Initial setup."); - expect(selfTestResults.find("p").text()).to.equal("success: true"); - }); - }); - - describe("Unsuccessful self tests", () => { - beforeEach(() => { - wrapper = mount( - - ); - }); - - it("should display the base error message when attempting to run self tests", () => { - wrapper = shallow( - - ); - - const description = wrapper.find(".description"); - - expect(description.text().trim()).to.equal( - "Exception getting self-test results for collection ..." - ); - }); - - it("should display information about the whole self test result", () => { - const passSVGIcon = wrapper.find(CheckSoloIcon); - const failSVGIcon = wrapper.find(XIcon); - const description = wrapper.find(".description"); - - // There are two self tests but one of them failed, so show a failing icon. - expect(failSVGIcon.length).to.equal(1); - expect(passSVGIcon.length).to.equal(0); - - const date = new Date(collections[1].self_test_results.start); - const expectedDate = getExpectedDate(date); - const expectedTime = getExpectedTime(date); - expect(description.text().trim()).to.equal( - `Tests last ran on ${expectedDate} ${expectedTime} and lasted 1.75s.` - ); - }); - - it("should display detail information for each self test result for the collection", () => { - const list = wrapper.find("ul"); - const selfTestResults = list.find("li"); - - expect(selfTestResults.length).to.equal(2); - expect(list.childAt(1).find("li").hasClass("success")).to.equal(true); - expect(list.childAt(1).find("h4").text()).to.equal("Initial setup."); - expect(list.childAt(1).find(".success-description").text()).to.equal( - "success: true" - ); - expect(list.childAt(1).find(".exception-description").length).to.equal(0); - - expect(list.childAt(2).find("li").hasClass("failure")).to.equal(true); - expect(list.childAt(2).find("h4").text()).to.equal( - "Acquiring test patron credentials." - ); - expect(list.childAt(2).find(".success-description").text()).to.equal( - "success: false" - ); - expect(list.childAt(2).find(".exception-description").text()).to.equal( - "exception: Collection is not associated with any libraries." - ); - }); - }); - - describe("Get new results", () => { - let runSelfTests; - let getSelfTests; - - beforeEach(() => { - runSelfTests = stub().returns( - new Promise((resolve) => resolve()) - ); - getSelfTests = stub().returns( - new Promise((resolve) => resolve()) - ); - wrapper = mount( - - ); - }); - - it("should run new self tests", async () => { - const runSelfTestsBtn = wrapper - .find("button") - .findWhere((el) => el.text() === "Run tests") - .at(0); - - expect(runSelfTests.callCount).to.equal(0); - - runSelfTestsBtn.simulate("click"); - - expect(runSelfTests.callCount).to.equal(1); - }); - - it("should run new self tests but get an error", async () => { - const error = { - status: 400, - response: "Failed to run new tests.", - url: "/admin/collection_self_tests/12", - }; - runSelfTests = stub().returns( - new Promise((resolve, reject) => reject(error)) - ); - getSelfTests = stub().returns( - new Promise((resolve) => resolve()) - ); - wrapper = mount( - - ); - const runSelfTestsBtn = wrapper - .find("button") - .findWhere((el) => el.text() === "Run tests") - .at(0); - let alert = wrapper.find(".alert"); - - expect(runSelfTests.callCount).to.equal(0); - expect(wrapper.state("error")).to.equal(null); - expect(alert.length).to.equal(0); - - runSelfTestsBtn.simulate("click"); - - const pause = (): Promise => { - return new Promise((resolve) => setTimeout(resolve, 0)); - }; - await pause(); - - expect(runSelfTests.callCount).to.equal(1); - expect(wrapper.state("error")).to.eq(error); - - alert = wrapper.render().find(".alert"); - expect(alert.length).to.equal(1); - }); - }); - describe("Handle metadata test results", () => { - const collectionNames = ["A", "B", "C"]; - const baseResult = collections[0].self_test_results.results[0]; - const results = []; - const makeResult = (c: string, success = true) => { - return { - ...baseResult, - ...{ - collection: c, - name: `Test ${results.length < collectionNames.length ? 1 : 2}`, - success: success, - }, - }; - }; - const display = (results) => - wrapper.instance().displayByCollection(results, false); - it("should call displayMetadata", () => { - const spyDisplayByCollection = spy( - wrapper.instance(), - "displayByCollection" - ); - expect(spyDisplayByCollection.callCount).to.equal(0); - const integration = { ...collections[0], ...{ goal: "metadata" } }; - wrapper.setState({ mostRecent: integration }); - wrapper.setProps({ sortByCollection: true }); - expect(spyDisplayByCollection.callCount).to.equal(1); - expect(spyDisplayByCollection.args[0][0][0]).to.equal(baseResult); - spyDisplayByCollection.restore(); - }); - it("should sort metadata test results by their collection", () => { - while (results.length < collectionNames.length * 2) { - collectionNames.map((c) => results.push(makeResult(c))); - } - const collectionPanels = display(results); - expect(collectionPanels.length).to.equal(collectionNames.length); - collectionPanels.map((panel: JSX.Element, idx: number) => { - const collectionName = collectionNames[idx]; - const { headerText, style, content } = panel.props; - expect(headerText).to.equal(collectionName); - expect(style).to.equal("success"); - content.map((x: JSX.Element, idx: number) => { - expect(x.props.result.collection).to.equal(collectionName); - expect(x.props.result.name).to.equal(`Test ${idx + 1}`); - }); - }); - }); - it("should display the result of the initial setup test", () => { - const initialResult = makeResult("undefined"); - const panel = display([initialResult])[0]; - expect(panel.props.headerText).to.equal("Initial Setup"); - }); - it("should add the 'danger' class if not all the tests for the collection succeeded", () => { - const errorResult = makeResult("With Error", false); - const successResult = makeResult("With Error"); - const panel = display([errorResult, successResult])[0]; - expect(panel.props.style).to.equal("danger"); - }); - }); -}); diff --git a/src/components/__tests__/ServiceEditForm-test.tsx b/src/components/__tests__/ServiceEditForm-test.tsx deleted file mode 100644 index d2fc8c1afc..0000000000 --- a/src/components/__tests__/ServiceEditForm-test.tsx +++ /dev/null @@ -1,1087 +0,0 @@ -import { expect } from "chai"; -import { stub, spy } from "sinon"; - -import * as React from "react"; -import { shallow, mount } from "enzyme"; - -import ServiceEditForm, { - ServiceEditFormProps, - ServiceEditFormState, -} from "../ServiceEditForm"; -import EditableInput from "../EditableInput"; -import ProtocolFormField from "../ProtocolFormField"; -import NeighborhoodAnalyticsForm from "../NeighborhoodAnalyticsForm"; -import { Button, Form } from "library-simplified-reusable-components"; -import WithRemoveButton from "../WithRemoveButton"; -import WithEditButton from "../WithEditButton"; -import { ServicesData } from "../../interfaces"; - -describe("ServiceEditForm", () => { - const TestServiceEditForm: new ( - props: ServiceEditFormProps - ) => React.Component< - ServiceEditFormProps, - ServiceEditFormState - > = ServiceEditForm; - let wrapper; - let save; - const urlBase = "/services"; - const serviceData = { - id: 2, - protocol: "protocol 1", - settings: { - text_setting: "text setting", - select_setting: "option2", - }, - libraries: [ - { - short_name: "nypl", - library_text_setting: "library text setting", - library_select_setting: "option4", - }, - ], - }; - const parentProtocol = { - name: "protocol 3", - label: "protocol 3 label", - settings: [{ key: "parent_setting", label: "parent label" }], - child_settings: [{ key: "child_setting", label: "child label" }], - library_settings: [], - }; - const protocolsData = [ - { - name: "protocol 1", - label: "protocol 1 label", - description: "protocol 1 description", - sitewide: false, - settings: [ - { key: "text_setting", label: "text label", optional: true }, - { - key: "select_setting", - label: "select label", - type: "select", - options: [ - { key: "option1", label: "option 1" }, - { key: "option2", label: "option 2" }, - ], - }, - ], - library_settings: [ - { - key: "library_text_setting", - label: "library text label", - optional: true, - }, - { - key: "library_select_setting", - label: "library select label", - type: "select", - options: [ - { key: "option3", label: "option 3" }, - { key: "option4", label: "option 4" }, - ], - }, - ], - }, - { - name: "protocol 2", - label: "protocol 2 label", - description: "protocol 2 description", - sitewide: true, - settings: [ - { key: "text_setting", label: "text label" }, - { key: "protocol2_setting", label: "protocol2 label" }, - ], - library_settings: [], - }, - { - name: "protocol with instructions", - label: "instructions label", - description: "click for instructions", - instructions: "Instructions!", - sitewide: false, - settings: [], - library_settings: [], - }, - parentProtocol, - ]; - const allLibraries = [ - { short_name: "nypl", name: "New York Public Library" }, - { short_name: "bpl", name: "Brooklyn Public Library" }, - ]; - const servicesData = { - services: [serviceData], - protocols: protocolsData, - allLibraries: allLibraries, - }; - - const editableInputByName = (name) => { - const inputs = wrapper.find(EditableInput); - if (inputs.length >= 1) { - return inputs.filterWhere((input) => input.props().name === name); - } - return []; - }; - - const protocolFormFieldByKey = (key) => { - const formFields = wrapper.find(ProtocolFormField); - if (formFields.length >= 1) { - return formFields.filterWhere( - (formField) => formField.props().setting.key === key - ); - } - return []; - }; - - describe("rendering", () => { - beforeEach(() => { - save = stub(); - wrapper = mount( - - ); - }); - - it("renders hidden id", () => { - // prettier-ignore - let input = wrapper.find("input[name=\"id\"]"); - expect(input.length).to.equal(0); - - wrapper.setProps({ item: serviceData }); - // prettier-ignore - input = wrapper.find("input[name=\"id\"]"); - expect(input.props().type).to.equal("hidden"); - expect(input.props().value).to.equal("2"); - }); - - it("renders protocol", () => { - let input = editableInputByName("protocol"); - // starts with first protocol in list - expect(input.props().value).to.equal("protocol 1"); - expect(input.props().readOnly).to.equal(false); - expect(input.props().description).to.equal("protocol 1 description"); - let children = input.find("option"); - expect(children.length).to.equal(4); - expect(children.at(0).text()).to.contain("protocol 1 label"); - expect(children.at(1).text()).to.contain("protocol 2 label"); - expect(children.at(2).text()).to.contain("instructions label"); - - wrapper = mount( - - ); - input = editableInputByName("protocol"); - expect(input.props().value).to.equal("protocol 1"); - expect(input.props().readOnly).to.equal(true); - expect(input.props().description).to.equal("protocol 1 description"); - children = input.find("option"); - expect(children.length).to.equal(1); - expect(children.text()).to.contain("protocol 1 label"); - }); - - it("renders protocol fields", () => { - let input = protocolFormFieldByKey("text_setting"); - expect(input.props().value).not.to.be.ok; - expect(input.props().setting).to.equal(protocolsData[0].settings[0]); - - input = protocolFormFieldByKey("select_setting"); - expect(input.props().value).not.to.be.ok; - expect(input.props().setting).to.equal(protocolsData[0].settings[1]); - - input = protocolFormFieldByKey("protocol2_setting"); - expect(input.length).to.equal(0); - - wrapper = mount( - - ); - - input = protocolFormFieldByKey("text_setting"); - expect(input.props().value).to.equal("text setting"); - expect(input.props().setting).to.equal(protocolsData[0].settings[0]); - - input = protocolFormFieldByKey("select_setting"); - expect(input.props().value).to.equal("option2"); - - input = protocolFormFieldByKey("protocol2_setting"); - expect(input.length).to.equal(0); - }); - - it("renders a collapsible component", () => { - wrapper = mount( - - ); - wrapper.setState({ protocol: "protocol with instructions" }); - - const collapsible = wrapper.find(".panel"); - expect(collapsible.length).to.equal(3); - const title = collapsible.at(0).find(".panel-title"); - expect(title.text()).to.equal("click for instructions"); - const body = collapsible.at(0).find(".panel-body"); - expect(body.text()).to.equal("Instructions!"); - }); - - it("doesn't render parent dropdown for protocol with no child settings", () => { - const input = editableInputByName("parent_id"); - expect(input.length).to.equal(0); - }); - - it("doesn't render parent dropdown when there are no available parents", () => { - const newService = Object.assign({}, serviceData, { - protocol: "protocol 3", - }); - wrapper = mount( - - ); - - const input = editableInputByName("parent_id"); - expect(input.length).to.equal(0); - }); - - it("renders parent dropdown for protocol with child settings and available parents", () => { - const parentService = Object.assign({}, serviceData, { - protocol: "protocol 3", - name: "Parent", - }); - const childService = Object.assign({}, serviceData, { - protocol: "protocol 3", - id: 3, - name: "Child", - }); - const servicesDataWithParent = Object.assign({}, servicesData, { - services: [parentService, childService], - }); - wrapper = mount( - - ); - - let input = editableInputByName("parent_id"); - expect(input.length).to.equal(1); - expect(input.props().value).to.equal(null); - let children = input.find("option"); - expect(children.length).to.equal(2); - expect(children.at(0).text()).to.contain("None"); - expect(children.at(1).text()).to.contain("Parent"); - - childService["parent_id"] = parentService.id; - wrapper = mount( - - ); - input = editableInputByName("parent_id"); - expect(input.length).to.equal(1); - expect(input.props().value).to.equal("2"); - children = input.find("option"); - expect(children.length).to.equal(2); - expect(children.at(0).text()).to.contain("None"); - expect(children.at(1).text()).to.contain("Parent"); - }); - - it("renders protocol fields for child", () => { - const parentService = Object.assign({}, serviceData, { - protocol: "protocol 3", - name: "Parent", - }); - const childService = Object.assign({}, serviceData, { - protocol: "protocol 3", - id: 3, - name: "Child", - }); - const servicesDataWithParent = Object.assign({}, servicesData, { - services: [parentService, childService], - }); - wrapper = mount( - - ); - - let input = protocolFormFieldByKey("parent_setting"); - expect(input.props().value).not.to.be.ok; - expect(input.props().setting).to.equal(parentProtocol.settings[0]); - - input = protocolFormFieldByKey("child_setting"); - expect(input.length).to.equal(0); - - childService.settings["parent_setting"] = "parent setting"; - wrapper.setProps({ item: childService }); - - input = protocolFormFieldByKey("parent_setting"); - expect(input.props().value).to.equal("parent setting"); - - childService["parent_id"] = parentService.id; - wrapper = mount( - - ); - - input = protocolFormFieldByKey("parent_setting"); - expect(input.length).to.equal(0); - - input = protocolFormFieldByKey("child_setting"); - expect(input.props().value).not.to.be.ok; - expect(input.props().setting).to.equal(parentProtocol.child_settings[0]); - - childService.settings["child_setting"] = "child setting"; - wrapper.setProps({ item: childService }); - - input = protocolFormFieldByKey("child_setting"); - expect(input.props().value).to.equal("child setting"); - }); - - it("doesn't render libraries for sitewide protocol without library settings", () => { - const servicesDataSitewide = Object.assign({}, servicesData, { - protocols: [servicesData.protocols[1]], - }); - - wrapper = mount( - - ); - let library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(0); - - const serviceDataSitewide = Object.assign({}, servicesData, { - libraries: [], - protocol: "", - }); - wrapper = mount( - - ); - library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(0); - }); - - it("renders libraries for a sitewide protocol with library settings", () => { - const servicesDataSitewide = Object.assign({}, servicesData, { - protocols: [ - Object.assign({}, servicesData.protocols[1], { sitewide: true }), - ], - }); - - wrapper = mount( - - ); - const library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(1); - }); - - it("renders removable and editable libraries", () => { - let library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(0); - - wrapper = mount( - - ); - library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(1); - const editable = library.find(WithEditButton); - expect(editable.length).to.equal(1); - expect(editable.props().children).to.contain("New York Public Library"); - }); - - it("renders removable but not editable libraries", () => { - let library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(0); - - const newServiceData = Object.assign({}, serviceData, { - protocol: "protocol 3", - }); - - wrapper = mount( - - ); - library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(1); - expect(library.props().children).to.contain("New York Public Library"); - }); - - it("doesn't render library add dropdown for sitewide protocol", () => { - const servicesDataSitewide = Object.assign({}, servicesData, { - protocols: [servicesData.protocols[1]], - }); - - wrapper = shallow( - - ); - let select = wrapper.find("select[name='add-library']"); - expect(select.length).to.equal(0); - - const serviceDataSitewide = Object.assign({}, servicesData, { - libraries: [], - protocol: "", - }); - wrapper = shallow( - - ); - select = wrapper.find("select[name='add-library']"); - expect(select.length).to.equal(0); - }); - - it("renders library add dropdown for a sitewide protocol with library settings", () => { - const servicesDataSitewide = Object.assign({}, servicesData, { - protocols: [ - Object.assign({}, servicesData.protocols[1], { sitewide: true }), - ], - }); - - wrapper = mount( - - ); - const select = wrapper.find("select[name='add-library']"); - expect(select.length).to.equal(1); - }); - - it("renders library add dropdown for a non-sitewide protocol", () => { - let select = editableInputByName("add-library"); - expect(select.props().label).to.equal("Add Library"); - - let options = select.find("option"); - expect(options.length).to.equal(3); - expect(options.at(0).props().value).to.equal("none"); - expect(options.at(1).props().value).to.equal("nypl"); - expect(options.at(2).props().value).to.equal("bpl"); - - let input = protocolFormFieldByKey("library_text_setting"); - expect(input.length).to.equal(0); - input = protocolFormFieldByKey("library_select_setting"); - expect(input.length).to.equal(0); - - wrapper = mount( - - ); - select = editableInputByName("add-library"); - expect(select.props().label).to.equal("Add Library"); - - options = select.find("option"); - expect(options.length).to.equal(2); - expect(options.at(0).props().value).to.equal("none"); - expect(options.at(1).props().value).to.equal("bpl"); - }); - - it("renders neighborhood analytics form for a relevant patron auth protocol", () => { - let neighborhoodForm = wrapper.find(NeighborhoodAnalyticsForm); - expect(neighborhoodForm.length).to.equal(0); - const patronAuthProtocol = { - ...protocolsData[0], - ...{ settings: [{ key: "neighborhood_mode", options: [] }] }, - }; - wrapper.setProps({ - extraFormKey: "neighborhood_mode", - extraFormSection: NeighborhoodAnalyticsForm, - data: { ...servicesData, ...{ protocols: [patronAuthProtocol] } }, - }); - neighborhoodForm = wrapper.find(NeighborhoodAnalyticsForm); - expect(neighborhoodForm.length).to.equal(1); - }); - - it("renders neighborhood analytics form for a relevant analytics protocol", () => { - let neighborhoodForm = wrapper.find(NeighborhoodAnalyticsForm); - expect(neighborhoodForm.length).to.equal(0); - const analyticsProtocol = { - ...protocolsData[0], - ...{ settings: [{ key: "location_source", options: [] }] }, - }; - wrapper.setProps({ - extraFormKey: "location_source", - extraFormSection: NeighborhoodAnalyticsForm, - data: { ...servicesData, ...{ protocols: [analyticsProtocol] } }, - }); - neighborhoodForm = wrapper.find(NeighborhoodAnalyticsForm); - expect(neighborhoodForm.length).to.equal(1); - }); - - it("has a save button", () => { - const saveButton = wrapper.find(Button); - expect(saveButton.length).to.equal(1); - }); - }); - - describe("behavior", () => { - beforeEach(() => { - save = stub().returns( - new Promise((resolve) => resolve()) - ); - wrapper = mount( - - ); - }); - - it("changes fields and description when protocol changes", () => { - // Select a library so the library settings are shown. - const librarySelect = wrapper.find("select[name='add-library']") as any; - librarySelect.getDOMNode().value = "nypl"; - librarySelect.simulate("change"); - - let textSettingInput = protocolFormFieldByKey("text_setting"); - let selectSettingInput = protocolFormFieldByKey("select_setting"); - let libraryTextSettingInput = protocolFormFieldByKey( - "library_text_setting" - ); - let librarySelectSettingInput = protocolFormFieldByKey( - "library_select_setting" - ); - let protocol2SettingInput = protocolFormFieldByKey("protocol2_setting"); - expect(textSettingInput.length).to.equal(1); - expect(selectSettingInput.length).to.equal(1); - expect(libraryTextSettingInput.length).to.equal(1); - expect(librarySelectSettingInput.length).to.equal(1); - expect(protocol2SettingInput.length).to.equal(0); - - let protocolInput = editableInputByName("protocol"); - expect(protocolInput.prop("description")).to.equal( - "protocol 1 description" - ); - - const select = wrapper.find("select[name='protocol']") as any; - const selectElement = select.getDOMNode(); - selectElement.value = "protocol 2"; - select.simulate("change"); - - textSettingInput = protocolFormFieldByKey("text_setting"); - selectSettingInput = protocolFormFieldByKey("select_setting"); - libraryTextSettingInput = protocolFormFieldByKey("library_text_setting"); - librarySelectSettingInput = protocolFormFieldByKey( - "library_select_setting" - ); - protocol2SettingInput = protocolFormFieldByKey("protocol2_setting"); - expect(textSettingInput.length).to.equal(1); - expect(selectSettingInput.length).to.equal(0); - expect(libraryTextSettingInput.length).to.equal(0); - expect(librarySelectSettingInput.length).to.equal(0); - expect(protocol2SettingInput.length).to.equal(1); - - protocolInput = editableInputByName("protocol"); - expect(protocolInput.prop("description")).to.equal( - "protocol 2 description" - ); - expect(protocolInput.prop("link")).to.be.undefined; - - selectElement.value = "protocol 1"; - select.simulate("change"); - - textSettingInput = protocolFormFieldByKey("text_setting"); - selectSettingInput = protocolFormFieldByKey("select_setting"); - libraryTextSettingInput = protocolFormFieldByKey("library_text_setting"); - librarySelectSettingInput = protocolFormFieldByKey( - "library_select_setting" - ); - protocol2SettingInput = protocolFormFieldByKey("protocol2_setting"); - expect(textSettingInput.length).to.equal(1); - expect(selectSettingInput.length).to.equal(1); - expect(libraryTextSettingInput.length).to.equal(1); - expect(librarySelectSettingInput.length).to.equal(1); - expect(protocol2SettingInput.length).to.equal(0); - - protocolInput = editableInputByName("protocol"); - expect(protocolInput.prop("description")).to.equal( - "protocol 1 description" - ); - }); - - it("changes fields when parent changes", () => { - const parentService = Object.assign({}, serviceData, { - protocol: "protocol 3", - name: "Parent", - }); - const servicesDataWithParent = Object.assign({}, servicesData, { - services: [parentService], - protocols: [parentProtocol], - }); - wrapper = mount( - - ); - let input = protocolFormFieldByKey("parent_setting"); - expect(input.length).to.equal(1); - - input = protocolFormFieldByKey("child_setting"); - expect(input.length).to.equal(0); - - const select = wrapper.find("select[name='parent_id']") as any; - const selectElement = select.getDOMNode(); - selectElement.value = "2"; - select.simulate("change"); - - input = protocolFormFieldByKey("parent_setting"); - expect(input.length).to.equal(0); - - input = protocolFormFieldByKey("child_setting"); - expect(input.length).to.equal(1); - - selectElement.value = ""; - select.simulate("change"); - - input = protocolFormFieldByKey("parent_setting"); - expect(input.length).to.equal(1); - - input = protocolFormFieldByKey("child_setting"); - expect(input.length).to.equal(0); - }); - - it("adds a library with settings", () => { - let library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(0); - - let libraryTextSettingInput = editableInputByName( - "library_text_setting" - ).find("input"); - expect(libraryTextSettingInput.length).to.equal(0); - let librarySelectSettingInput = editableInputByName( - "library_select_setting" - ).find("select"); - expect(librarySelectSettingInput.length).to.equal(0); - - let select = wrapper.find("select[name='add-library']").hostNodes(); - select.getDOMNode().value = "bpl"; - select.simulate("change"); - - libraryTextSettingInput = editableInputByName( - "library_text_setting" - ).find("input"); - expect(libraryTextSettingInput.length).to.equal(1); - librarySelectSettingInput = editableInputByName( - "library_select_setting" - ).find("select"); - expect(librarySelectSettingInput.length).to.equal(1); - - select.getDOMNode().value = "none"; - select.simulate("change"); - - libraryTextSettingInput = editableInputByName( - "library_text_setting" - ).find("input"); - expect(libraryTextSettingInput.length).to.equal(0); - librarySelectSettingInput = editableInputByName( - "library_select_setting" - ).find("select"); - expect(librarySelectSettingInput.length).to.equal(0); - - select.getDOMNode().value = "bpl"; - select.simulate("change"); - - libraryTextSettingInput = editableInputByName( - "library_text_setting" - ).find("input"); - libraryTextSettingInput.getDOMNode().value = "library text"; - libraryTextSettingInput.simulate("change"); - librarySelectSettingInput = editableInputByName( - "library_select_setting" - ).find("select"); - librarySelectSettingInput.getDOMNode().value = "option4"; - librarySelectSettingInput.simulate("change"); - - const addButton = wrapper - .find("button") - .findWhere((el) => el.text() === "Add Library") - .hostNodes(); - addButton.simulate("click"); - - library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(1); - expect(library.text()).to.contain("Brooklyn Public Library"); - expect(library.text()).to.contain("Delete"); - expect(library.text()).to.contain("Edit"); - - const stateLibraries = wrapper.state().libraries; - expect(stateLibraries.length).to.equal(1); - expect(stateLibraries[0].short_name).to.equal("bpl"); - expect(stateLibraries[0].library_text_setting).to.equal("library text"); - expect(stateLibraries[0].library_select_setting).to.equal("option4"); - - select = wrapper.find("select[name='add-library']").hostNodes(); - select.getDOMNode().value = "nypl"; - select.simulate("change"); - - libraryTextSettingInput = editableInputByName( - "library_text_setting" - ).find("input"); - expect(libraryTextSettingInput.getDOMNode().value).to.equal(""); - librarySelectSettingInput = editableInputByName( - "library_select_setting" - ).find("select"); - expect(librarySelectSettingInput.getDOMNode().value).to.equal("option3"); - }); - - it("removes a library", () => { - wrapper = mount( - - ); - let library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(1); - expect(library.text()).to.contain("New York Public Library"); - - const onRemove = library.prop("onRemove"); - onRemove(); - wrapper.update(); - - library = wrapper.find(WithRemoveButton); - expect(library.length).to.equal(0); - }); - - it("edits a library", () => { - wrapper = mount( - - ); - const library = wrapper.find(WithRemoveButton).find(".with-edit-button"); - expect(library.length).to.equal(1); - expect(library.text()).to.contain("New York Public Library"); - - const onEdit = library.find("button"); - onEdit.simulate("click"); - - let settings = wrapper.find(".edit-library-settings"); - expect(settings.length).to.equal(1); - let libraryTextSettingInput = settings.find( - "input[name='library_text_setting']" - ) as any; - expect(libraryTextSettingInput.getDOMNode().value).to.equal( - "library text setting" - ); - let librarySelectSettingInput = settings.find( - "select[name='library_select_setting']" - ) as any; - expect(librarySelectSettingInput.getDOMNode().value).to.equal("option4"); - - libraryTextSettingInput.getDOMNode().value = "new library text"; - libraryTextSettingInput.simulate("change"); - librarySelectSettingInput.getDOMNode().value = "option3"; - librarySelectSettingInput.simulate("change"); - - onEdit.simulate("click"); - - settings = wrapper.find(".edit-library-settings"); - expect(settings.length).to.equal(0); - - onEdit.simulate("click"); - - settings = wrapper.find(".edit-library-settings"); - expect(settings.length).to.equal(1); - libraryTextSettingInput = settings.find( - "input[name='library_text_setting']" - ) as any; - expect(libraryTextSettingInput.getDOMNode().value).to.equal( - "library text setting" - ); - librarySelectSettingInput = settings.find( - "select[name='library_select_setting']" - ) as any; - expect(librarySelectSettingInput.getDOMNode().value).to.equal("option4"); - - libraryTextSettingInput.getDOMNode().value = "new library text"; - libraryTextSettingInput.simulate("change"); - librarySelectSettingInput.getDOMNode().value = "option3"; - librarySelectSettingInput.simulate("change"); - - const saveButton = settings.find("button"); - saveButton.simulate("click"); - - const stateLibraries = wrapper.state().libraries; - expect(stateLibraries.length).to.equal(1); - expect(stateLibraries[0].short_name).to.equal("nypl"); - expect(stateLibraries[0].library_text_setting).to.equal( - "new library text" - ); - expect(stateLibraries[0].library_select_setting).to.equal("option3"); - }); - - it("calls save when the save button is clicked", () => { - const saveButton = wrapper.find(Button); - saveButton.simulate("click"); - expect(save.callCount).to.equal(1); - }); - - it("calls save when the form is submitted directly", () => { - wrapper.find(Form).props().onSubmit(); - expect(save.callCount).to.equal(1); - }); - - it("calls save on submit even if there is a collapsible panel", () => { - wrapper.setState({ protocol: "protocol with instructions" }); - const collapsible = wrapper.find(".panel"); - expect(collapsible.length).to.equal(3); - - wrapper.find(Form).props().onSubmit(); - expect(save.callCount).to.equal(1); - }); - - it("calls handleData", () => { - const handleData = spy(wrapper.instance(), "handleData"); - wrapper.setProps({ item: serviceData }); - // The first two buttons are the edit and remove buttons for this component - // which only contains one object as data. - wrapper.find(Button).at(2).simulate("click"); - - expect(handleData.callCount).to.equal(1); - - handleData.restore(); - }); - - it("submits data", () => { - wrapper.setProps({ item: serviceData }); - - // The first two buttons are the edit and remove buttons for this component - // which only contains one object as data. - const saveButton = wrapper.find(Button).at(2); - saveButton.simulate("click"); - - expect(save.callCount).to.equal(1); - const formData = save.args[0][0]; - expect(formData.get("id")).to.equal("2"); - expect(formData.get("protocol")).to.equal("protocol 1"); - expect(formData.get("text_setting")).to.equal("text setting"); - expect(formData.get("select_setting")).to.equal("option2"); - expect(formData.get("libraries")).to.equal( - JSON.stringify(serviceData.libraries) - ); - }); - - const fillOutFormFields = () => { - const nameInput = wrapper.find("input[name='name']"); - const nameInputElement = nameInput.getDOMNode(); - nameInputElement.value = "new service"; - nameInput.simulate("change"); - }; - - it("clears the form", () => { - fillOutFormFields(); - let nameInput = wrapper.find("input[name='name']"); - expect(nameInput.props().value).to.equal("new service"); - - wrapper.simulate("submit"); - const newProps = { responseBody: "new service", ...wrapper.props() }; - wrapper.setProps(newProps); - - nameInput = wrapper.find("input[name='name']"); - expect(nameInput.props().value).to.equal(""); - }); - - it("doesn't clear the form if there's an error message", () => { - fillOutFormFields(); - let nameInput = wrapper.find("input[name='name']"); - - expect(nameInput.props().value).to.equal("new service"); - - wrapper.simulate("submit"); - const newProps = { fetchError: "ERROR", ...wrapper.props() }; - wrapper.setProps(newProps); - - nameInput = wrapper.find("input[name='name']"); - expect(nameInput.props().value).to.equal("new service"); - }); - }); - - describe("library removal confirmation", () => { - let serviceEditFormInstance; - let isLibraryRemovalPermittedSpy; - let removeLibrarySpy; - - beforeEach(() => { - save = stub().returns(new Promise((resolve) => resolve())); - wrapper = mount( - - ); - serviceEditFormInstance = wrapper.instance(); - removeLibrarySpy = spy(serviceEditFormInstance, "removeLibrary"); - }); - - afterEach(() => { - isLibraryRemovalPermittedSpy.restore(); - removeLibrarySpy.restore(); - }); - - const shouldRemoveLibrary = () => { - const libraryToRemove = serviceData.libraries[0]; - const removeButton = wrapper.find(WithRemoveButton).at(0).find("button.remove-btn"); - - removeButton.simulate("click"); - - expect(isLibraryRemovalPermittedSpy.calledOnce).to.be.true; - expect(isLibraryRemovalPermittedSpy.calledWith(libraryToRemove)).to.be.true; - expect(isLibraryRemovalPermittedSpy.returned(true)).to.be.true; - - // Library removal should be performed. - expect(removeLibrarySpy.calledOnce).to.be.true; - expect(removeLibrarySpy.calledWith(libraryToRemove)).to.be.true; - - // Ensure that the library was removed from state. - wrapper.update(); - expect(wrapper.state("libraries")).to.deep.equal([]); - }; - - it("should allow removal by default", () => { - isLibraryRemovalPermittedSpy = spy(serviceEditFormInstance, "isLibraryRemovalPermitted"); - shouldRemoveLibrary(); - }); - - it("should allow remove if isLibraryRemovalPermitted returns true", () => { - isLibraryRemovalPermittedSpy = stub(serviceEditFormInstance, "isLibraryRemovalPermitted").returns(true); - shouldRemoveLibrary(); - }); - - it("should not allow remove if isLibraryRemovalPermitted returns false", () => { - isLibraryRemovalPermittedSpy = stub(serviceEditFormInstance, "isLibraryRemovalPermitted").returns(false); - const libraryToRemove = serviceData.libraries[0]; - const removeButton = wrapper.find(WithRemoveButton).at(0).find("button.remove-btn"); - - removeButton.simulate("click"); - - expect(isLibraryRemovalPermittedSpy.calledOnce).to.be.true; - expect(isLibraryRemovalPermittedSpy.calledWith(libraryToRemove)).to.be.true; - expect(isLibraryRemovalPermittedSpy.returned(false)).to.be.true; - - // Library disassociation should NOT be performed. - expect(removeLibrarySpy.notCalled).to.be.true; - - // Ensure that the library was NOT removed from state. - wrapper.update(); - expect(wrapper.state("libraries")).to.deep.equal(serviceData.libraries); - }); - }); -}); diff --git a/tests/jest/components/Collections.test.tsx b/tests/jest/components/Collections.test.tsx index 4ea5bbdd04..9cced2915e 100644 --- a/tests/jest/components/Collections.test.tsx +++ b/tests/jest/components/Collections.test.tsx @@ -1,19 +1,18 @@ import * as React from "react"; -import { fireEvent } from "@testing-library/react"; +import { installFormDataShim } from "../testUtils/formDataShim"; +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Collections } from "../../../src/components/Collections"; +import CollectionsConnected from "../../../src/components/Collections"; import renderWithContext from "../testUtils/renderWithContext"; +import { renderWithProviders } from "../testUtils/withProviders"; +import buildStore from "../../../src/store"; import { CollectionsData, ConfigurationSettings, } from "../../../src/interfaces"; import { defaultFeatureFlags } from "../../../src/utils/featureFlags"; -// NB: This adds tests to the already existing tests in: -// - `src/components/__tests__/Collections-test.tsx`. -// -// Those tests should eventually be migrated here and -// adapted to the Jest/React Testing Library paradigm. - describe("Collections - associated library disclosure", () => { // ── Shared fixtures ─────────────────────────────────────────────────────── @@ -219,3 +218,332 @@ describe("Collections - associated library disclosure", () => { ); }); }); + +// These exercise behavior the disclosure tests above do not: the connect() +// wiring (mapStateToProps / mapDispatchToProps, which only run when the CONNECTED +// default export mounts), the marked-for-deletion list item, the +// confirm-before-disassociating-libraries behavior, the +// getChildContext().registerLibrary closure, the on-mount registrations fetch, +// and the delete flow. + +describe("Collections - connected wiring, deletion, and registration", () => { + const allLibraries = [ + { short_name: "nypl", name: "NYPL", uuid: "uuid-nypl" }, + ]; + + const sysAdminConfig: Partial = { + csrfToken: "", + featureFlags: defaultFeatureFlags, + roles: [{ role: "system" }], + }; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("connected default export", () => { + // A loaded `libraries` slice persists through the on-mount collections fetch + // (the libraries reducer ignores that action), so mapStateToProps takes its + // `data.allLibraries` branch on every render. + const loadedLibrariesState = { + data: { libraries: allLibraries }, + isFetching: false, + isEditing: false, + fetchError: null, + formError: null, + isLoaded: true, + responseBody: null, + successMessage: null, + }; + + const stubFetch = (body: unknown) => + jest.spyOn(globalThis, "fetch").mockImplementation( + async () => + new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) + ); + + it("wires mapStateToProps/mapDispatchToProps and marks a collection for deletion", async () => { + const listData = { + collections: [ + { id: 2, protocol: "test protocol", name: "ODL" }, + { + id: 3, + protocol: "test protocol", + name: "Enki", + marked_for_deletion: true, + }, + ], + protocols: [{ name: "test protocol", label: "TP", settings: [] }], + }; + stubFetch(listData); + const store = buildStore({ + editor: { libraries: loadedLibrariesState }, + } as any); + + const { container } = renderWithProviders( + , + { + reduxProviderProps: { store }, + appConfigSettings: sysAdminConfig, + } + ); + + // The list appears once the connected component's on-mount fetch resolves + // and mapStateToProps feeds the fetched data back in as props. + expect( + await screen.findByRole("heading", { level: 3, name: "ODL" }) + ).toBeInTheDocument(); + + // The non-deleted collection has edit and delete controls. + const editLink = container.querySelector("a.edit-item"); + expect(editLink.getAttribute("href")).toBe( + "/admin/web/config/collections/edit/2" + ); + + // The deleted collection renders a special item with no edit/delete + // controls and an explanatory message. + const deleted = container.querySelector(".deleted-collection"); + expect(deleted).not.toBeNull(); + expect(deleted).toHaveTextContent("Enki"); + expect(deleted).toHaveTextContent(/cannot be edited/); + expect(deleted.querySelector("a.edit-item")).toBeNull(); + expect(deleted.querySelector("button.delete-item")).toBeNull(); + }); + }); + + describe("edit mode: registration and additional content", () => { + // The reusable-components `Form` builds `new FormData(formElement)` on + // submit, which the unit jsdom env's undici FormData rejects; install the + // shared shim that reads the form's successful controls. + installFormDataShim(); + it("fetches registrations on mount, renders task controls, and registers a library on click", async () => { + const user = userEvent.setup(); + const registerLibrary = jest.fn().mockResolvedValue(undefined); + const fetchLibraryRegistrations = jest.fn().mockResolvedValue(undefined); + renderWithContext( + , + sysAdminConfig + ); + + // componentDidMount fetches the registrations once. (CollectionEditForm's + // renderAdditionalContent also runs here, since the edited collection has + // an id, injecting the import/reap task panels into the form.) + expect(fetchLibraryRegistrations).toHaveBeenCalledTimes(1); + + // Registering a library goes through getChildContext().registerLibrary. + expect(registerLibrary).not.toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: "Register" })); + + expect(registerLibrary).toHaveBeenCalledTimes(1); + const formData = registerLibrary.mock.calls[0][0]; + expect(formData.get("library_short_name")).toBe("nypl"); + expect(formData.get("collection_id")).toBe("7"); + + // After a successful registration, the child context refetches. + await waitFor(() => + expect(fetchLibraryRegistrations).toHaveBeenCalledTimes(2) + ); + }); + }); + + describe("confirm before disassociating libraries", () => { + const initialLibraries = [ + { short_name: "palace", name: "Palace" }, + { short_name: "another-library", name: "Another Library" }, + ]; + + const renderEditForm = ( + libraries: Array<{ short_name: string }>, + libraryMeta: Array<{ short_name: string; name?: string }> + ) => + renderWithContext( + , + sysAdminConfig + ); + + it("removes the library when the confirmation is accepted", () => { + const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true); + const { container } = renderEditForm( + [...initialLibraries], + [...initialLibraries] + ); + + // One remove button per associated library. + expect(container.querySelectorAll("button.remove-btn")).toHaveLength(2); + + fireEvent.click(container.querySelectorAll("button.remove-btn")[0]); + + // The prompt names the (full) library being removed. + expect(confirmSpy).toHaveBeenCalledTimes(1); + expect(confirmSpy.mock.calls[0][0]).toBe( + 'Disassociating library "Palace" from this collection will ' + + "remove all loans and holds for its patrons. Do you wish to continue?" + ); + + // The association (and its remove button) is gone. + expect(container.querySelectorAll("button.remove-btn")).toHaveLength(1); + }); + + it("keeps the library when the confirmation is canceled", () => { + jest.spyOn(window, "confirm").mockReturnValue(false); + const { container } = renderEditForm( + [...initialLibraries], + [...initialLibraries] + ); + + expect(container.querySelectorAll("button.remove-btn")).toHaveLength(2); + fireEvent.click(container.querySelectorAll("button.remove-btn")[0]); + // Nothing was removed. + expect(container.querySelectorAll("button.remove-btn")).toHaveLength(2); + }); + + it("uses the library short_name in the prompt when the full name is unavailable", () => { + const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(false); + // The library is not present in allLibraries, so getLibrary() returns null. + const { container } = renderEditForm([{ short_name: "orphan" }], []); + + fireEvent.click(container.querySelector("button.remove-btn")); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + expect(confirmSpy.mock.calls[0][0]).toBe( + 'Disassociating library "orphan" from this collection will ' + + "remove all loans and holds for its patrons. Do you wish to continue?" + ); + }); + }); + + describe("create mode", () => { + it("renders the create form without import/reap task panels", () => { + // In create mode there is no saved collection, so CollectionEditForm's + // renderAdditionalContent short-circuits and injects no task panels. + renderWithContext( + , + sysAdminConfig + ); + + expect( + screen.getByRole("heading", { name: "Create a new collection" }) + ).toBeInTheDocument(); + }); + }); + + describe("deletion", () => { + it("deletes a collection and refetches after confirmation", async () => { + const deleteItem = jest.fn().mockResolvedValue(undefined); + const fetchData = jest.fn(); + const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true); + + const { container } = renderWithContext( + , + sysAdminConfig + ); + + fireEvent.click(container.querySelector("button.delete-item")); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + await waitFor(() => expect(deleteItem).toHaveBeenCalledWith(1)); + // fetchData is called once on mount; the post-delete refetch is the second + // call, so assert the count rather than mere invocation. + await waitFor(() => expect(fetchData).toHaveBeenCalledTimes(2)); + }); + }); +}); diff --git a/tests/jest/components/EditableConfigList.test.tsx b/tests/jest/components/EditableConfigList.test.tsx index 84e43c616a..533d83aa0d 100644 --- a/tests/jest/components/EditableConfigList.test.tsx +++ b/tests/jest/components/EditableConfigList.test.tsx @@ -1,18 +1,15 @@ import * as React from "react"; -import { fireEvent } from "@testing-library/react"; +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { EditableConfigList, EditFormProps, + AdditionalContentProps, } from "../../../src/components/EditableConfigList"; import renderWithContext from "../testUtils/renderWithContext"; import { ConfigurationSettings } from "../../../src/interfaces"; import { defaultFeatureFlags } from "../../../src/utils/featureFlags"; - -// NB: This adds tests to the already existing tests in: -// - `src/components/__tests__/EditableConfigList-test.tsx`. -// -// Those tests should eventually be migrated here and -// adapted to the Jest/React Testing Library paradigm. +import * as navigate from "../../../src/utils/navigate"; describe("EditableConfigList - library association disclosure", () => { // ── Test doubles ────────────────────────────────────────────────────────── @@ -403,3 +400,597 @@ describe("EditableConfigList - library association disclosure", () => { }); }); }); + +// These exercise the abstract base class through concrete test subclasses, +// asserting observable DOM instead of reaching into instances. +describe("EditableConfigList - base class via test subclasses", () => { + interface Thing { + id: number; + label: string; + level?: number; + } + + interface Things { + things: Thing[]; + } + + // Closure-controlled permission flags. + let canCreate: boolean; + let canDelete: boolean; + let canEdit: boolean; + + // A test EditForm that surfaces the props the base class passes to it, so + // prop-only assertions can be checked against the rendered DOM. + class ThingEditForm extends React.Component> { + render(): JSX.Element { + const { disabled, save, item, listDataKey, adminLevel } = this.props; + return ( +
+ {String(disabled)} + {String(!!save)} + {listDataKey} + {item ? String(item.id) : "none"} + {String(adminLevel)} + +
+ ); + } + } + + class ThingAdditionalContent extends React.Component< + AdditionalContentProps + > { + render(): JSX.Element { + return ( +
+ Test Additional Content +
+ ); + } + } + + // Uses the base implementations of canCreate/canDelete/canEdit. + class BaseThingList extends EditableConfigList { + EditForm = ThingEditForm; + listDataKey = "things"; + itemTypeName = "thing"; + urlBase = "/admin/things/"; + identifierKey = "id"; + labelKey = "label"; + } + + // Overrides permission checks from the closure flags. + class ThingEditableConfigList extends BaseThingList { + label(item): string { + return "test " + super.label(item); + } + canCreate() { + return canCreate; + } + canDelete() { + return canDelete; + } + canEdit() { + return canEdit; + } + } + + class OneThingEditableConfigList extends ThingEditableConfigList { + limitOne = true; + } + + class ThingAdditionalContentEditableConfigList extends ThingEditableConfigList { + AdditionalContent = ThingAdditionalContent; + } + + class CapsThingList extends BaseThingList { + itemTypeName = "CDN"; + } + + class ThingWithSelfTests extends ThingEditableConfigList { + links = { + info: ( + <> + Self-tests for the things have been moved to{" "} + + the troubleshooting page + + . + + ), + footer: ( + <> + Problems with your things? Please visit{" "} + + the troubleshooting page + + . + + ), + }; + } + + const thingData: Thing = { id: 5, label: "label" }; + const thingsData: Things = { things: [thingData] }; + + let fetchData: jest.Mock; + let editItem: jest.Mock; + let deleteItem: jest.Mock; + + const systemConfig: Partial = { + csrfToken: "token", + featureFlags: defaultFeatureFlags, + roles: [{ role: "system", library: "nypl" }], + }; + + const configWithRoles = ( + roles: Partial["roles"] + ): Partial => ({ + csrfToken: "token", + featureFlags: defaultFeatureFlags, + roles, + }); + + const baseProps = (overrides: Record = {}) => ({ + data: thingsData, + fetchData, + editItem, + deleteItem, + csrfToken: "token", + isFetching: false, + ...overrides, + }); + + beforeEach(() => { + fetchData = jest.fn(); + editItem = jest.fn().mockResolvedValue(undefined); + deleteItem = jest.fn().mockResolvedValue(undefined); + canCreate = true; + canDelete = true; + canEdit = true; + jest.spyOn(window, "scrollTo").mockImplementation(() => undefined); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("shows an error message if there's a problem loading the list", () => { + const { container, rerender } = renderWithContext( + , + systemConfig + ); + expect(container.querySelector(".alert-danger")).toBeNull(); + + const fetchError = { + status: 404, + response: "test load error", + url: "test url", + }; + rerender(); + expect(container.querySelector(".alert-danger")).not.toBeNull(); + expect(screen.getByText("Error: test load error")).toBeInTheDocument(); + + // Hidden when the user goes to the form. + rerender( + + ); + expect(container.querySelector(".alert-danger")).toBeNull(); + }); + + it("shows form submission error message only if the form is displayed", () => { + const formError = { + status: 400, + response: "test submission error", + url: "test url", + }; + const { container, rerender } = renderWithContext( + , + systemConfig + ); + // Not displayed while looking at the list. + expect(container.querySelector(".alert-danger")).toBeNull(); + + rerender( + + ); + expect(container.querySelector(".alert-danger")).not.toBeNull(); + expect( + screen.getByText("Error: test submission error") + ).toBeInTheDocument(); + + // Hidden when the user goes back to the list. + rerender( + + ); + expect(container.querySelector(".alert-danger")).toBeNull(); + }); + + it("shows a success message with an edit link on create", () => { + const { container } = renderWithContext( + , + systemConfig + ); + const success = container.querySelector(".alert-success"); + expect(success).not.toBeNull(); + expect(success.textContent).toBe("Successfully created a new thing"); + const link = success.querySelector("a"); + expect(link).not.toBeNull(); + expect(link.getAttribute("href")).toBe("/admin/things/edit/itemType"); + }); + + it("shows a success message without a link on edit", () => { + const { container } = renderWithContext( + , + systemConfig + ); + const success = container.querySelector(".alert-success"); + expect(success).not.toBeNull(); + expect(success.textContent).toBe("Successfully edited this thing"); + expect(success.querySelector("a")).toBeNull(); + }); + + it("keeps all-caps item type names uppercase in the success message", () => { + const { container } = renderWithContext( + , + systemConfig + ); + const success = container.querySelector(".alert-success"); + expect(success.textContent).toBe("Successfully created a new CDN"); + }); + + it("shows a loading indicator only while fetching", () => { + const { rerender } = renderWithContext( + , + systemConfig + ); + expect(screen.queryByRole("dialog")).toBeNull(); + rerender(); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + it("shows the thing header", () => { + renderWithContext( + , + systemConfig + ); + expect( + screen.getByRole("heading", { level: 2, name: "Thing configuration" }) + ).toBeInTheDocument(); + }); + + it("shows the thing list with an edit link and a count", () => { + const { container } = renderWithContext( + , + systemConfig + ); + const items = container.querySelectorAll("ul > li"); + expect(items).toHaveLength(1); + expect(items[0].textContent).toContain("test label"); + const editLink = items[0].querySelector(".edit-item"); + expect(editLink.getAttribute("href")).toBe("/admin/things/edit/5"); + expect( + container.querySelector(".list-container header div").textContent + ).toBe("1 configured"); + }); + + it("updates the thing list when new data arrives", () => { + const { container, rerender } = renderWithContext( + , + systemConfig + ); + const newThingsData = { + things: [thingData, { id: 6, label: "another thing" }], + }; + rerender( + + ); + const items = container.querySelectorAll("ul > li"); + expect(items).toHaveLength(2); + expect(items[1].textContent).toContain("test another thing"); + expect( + items[1] + .querySelector(".edit-item") + .getAttribute("href") + ).toBe("/admin/things/edit/6"); + }); + + it("shows the create link, which respects canCreate", () => { + const { container, rerender } = renderWithContext( + , + systemConfig + ); + const createLink = + container.querySelector(".create-item"); + expect(createLink.textContent).toBe("Create new thing"); + expect(createLink.getAttribute("href")).toBe("/admin/things/create"); + + canCreate = false; + rerender(); + expect(container.querySelector(".create-item")).toBeNull(); + }); + + it("uses the base canCreate (true) when a subclass does not override it", () => { + const { container } = renderWithContext( + , + systemConfig + ); + expect(container.querySelector(".create-item")).not.toBeNull(); + }); + + it("hides the create link if only one item is allowed and it already exists", () => { + const { container, rerender } = renderWithContext( + , + systemConfig + ); + expect(container.querySelector(".create-item")).toBeNull(); + + rerender( + + ); + const createLink = + container.querySelector(".create-item"); + expect(createLink).not.toBeNull(); + expect(createLink.getAttribute("href")).toBe("/admin/things/create"); + }); + + it("shows a view button instead of an edit button when canEdit is false", () => { + canEdit = false; + const { container } = renderWithContext( + , + configWithRoles([{ role: "manager", library: "nypl" }]) + ); + const editItemLink = container.querySelector(".edit-item"); + expect(editItemLink.textContent).toContain("View"); + expect(editItemLink.textContent).not.toContain("Edit"); + }); + + it("shows a delete button when canDelete is true and hides it otherwise", () => { + // Base canDelete: system admin (level 3) → delete button shown. + const shown = renderWithContext( + , + systemConfig + ); + expect(shown.container.querySelector(".delete-item")).not.toBeNull(); + shown.unmount(); + + // Base canDelete for a librarian (level 1) → no delete button. + const hiddenByRole = renderWithContext( + , + configWithRoles([{ role: "librarian", library: "nypl" }]) + ); + expect(hiddenByRole.container.querySelector(".delete-item")).toBeNull(); + hiddenByRole.unmount(); + + // Explicit override returning false → no delete button. + canDelete = false; + const hiddenByOverride = renderWithContext( + , + systemConfig + ); + expect(hiddenByOverride.container.querySelector(".delete-item")).toBeNull(); + }); + + it("deletes an item only after the user confirms", async () => { + const user = userEvent.setup(); + const confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(false); + const { container } = renderWithContext( + , + systemConfig + ); + const deleteButton = + container.querySelector(".delete-item"); + expect(deleteButton).not.toBeNull(); + + await user.click(deleteButton); + expect(deleteItem).toHaveBeenCalledTimes(0); + + confirmSpy.mockReturnValue(true); + await user.click(deleteButton); + await waitFor(() => expect(deleteItem).toHaveBeenCalledTimes(1)); + expect(deleteItem).toHaveBeenCalledWith(5); + }); + + it("renders the create form and its header", () => { + renderWithContext( + , + systemConfig + ); + expect( + screen.getByRole("heading", { level: 3, name: "Create a new thing" }) + ).toBeInTheDocument(); + expect(screen.getByTestId("ef-item")).toHaveTextContent("none"); + expect(screen.getByTestId("ef-disabled")).toHaveTextContent("false"); + expect(screen.getByTestId("ef-datakey")).toHaveTextContent("things"); + expect(screen.getByTestId("ef-has-save")).toHaveTextContent("true"); + }); + + it("renders the edit form with the item and an editable header", () => { + renderWithContext( + , + systemConfig + ); + expect( + screen.getByRole("heading", { level: 3, name: "Edit test label" }) + ).toBeInTheDocument(); + expect(screen.getByTestId("ef-item")).toHaveTextContent("5"); + expect(screen.getByTestId("ef-disabled")).toHaveTextContent("false"); + expect(screen.getByTestId("ef-has-save")).toHaveTextContent("true"); + }); + + it("shows a non-editable header, no save function, and a disabled form when canEdit is false", () => { + canEdit = false; + renderWithContext( + , + systemConfig + ); + expect( + screen.getByRole("heading", { level: 3, name: "test label" }) + ).toBeInTheDocument(); + expect(screen.getByTestId("ef-has-save")).toHaveTextContent("false"); + expect(screen.getByTestId("ef-disabled")).toHaveTextContent("true"); + }); + + it("updates the edit-form header when the item's label changes", () => { + const { rerender } = renderWithContext( + , + systemConfig + ); + expect( + screen.getByRole("heading", { level: 3, name: "Edit test label" }) + ).toBeInTheDocument(); + + rerender( + + ); + expect( + screen.getByRole("heading", { level: 3, name: "Edit test new thing!" }) + ).toBeInTheDocument(); + }); + + it("fetches on mount, passes a working save function, and refetches on save", async () => { + const user = userEvent.setup(); + renderWithContext( + , + systemConfig + ); + expect(fetchData).toHaveBeenCalledTimes(1); + expect(editItem).toHaveBeenCalledTimes(0); + + await user.click(screen.getByTestId("ef-save")); + expect(editItem).toHaveBeenCalledTimes(1); + await waitFor(() => expect(fetchData).toHaveBeenCalledTimes(2)); + }); + + it("does not fetch on mount if a fetch is already in progress", () => { + renderWithContext( + , + systemConfig + ); + expect(fetchData).toHaveBeenCalledTimes(0); + }); + + it("navigates to the edit page after successfully creating a limit-one item", async () => { + const user = userEvent.setup(); + const navigateSpy = jest + .spyOn(navigate, "navigateTo") + .mockImplementation(() => undefined); + renderWithContext( + , + systemConfig + ); + await user.click(screen.getByTestId("ef-save")); + await waitFor( + () => expect(navigateSpy).toHaveBeenCalledWith("/admin/things/edit/99"), + { timeout: 3000 } + ); + }); + + it("does not render AdditionalContent unless the subclass supplies it", () => { + const { container, unmount } = renderWithContext( + , + systemConfig + ); + expect(container.querySelector(".additional-content")).toBeNull(); + unmount(); + + const withContent = renderWithContext( + , + systemConfig + ); + const additional = withContent.container.querySelector( + ".additional-content" + ); + expect(additional).not.toBeNull(); + expect(additional.textContent).toBe("Test Additional Content"); + expect(additional.getAttribute("data-item-id")).toBe("5"); + expect(additional.getAttribute("data-csrf")).toBe("token"); + }); + + it("shows the troubleshooting link and info alert only when there are self-tests", () => { + const { container, unmount } = renderWithContext( + , + systemConfig + ); + // No self-tests configured → neither the info alert nor the footer link. + expect(container.querySelector(".alert-info")).toBeNull(); + expect(container.querySelector("p")).toBeNull(); + unmount(); + + const withSelfTests = renderWithContext( + , + systemConfig + ); + const info = withSelfTests.container.querySelector(".alert-info"); + expect(info.textContent).toBe( + "Self-tests for the things have been moved to the troubleshooting page." + ); + expect(info.querySelector("a").getAttribute("href")).toBe( + "/admin/web/troubleshooting/self-tests/thingServices" + ); + + const footer = withSelfTests.container.querySelector("p"); + expect(footer.textContent).toBe( + "Problems with your things? Please visit the troubleshooting page." + ); + expect(footer.querySelector("a").getAttribute("href")).toBe( + "/admin/web/troubleshooting/self-tests/thingServices" + ); + }); + + it("figures out what level of permissions the admin has", () => { + const adminLevelFor = (roles) => { + const { getByTestId, unmount } = renderWithContext( + , + configWithRoles(roles) + ); + const level = getByTestId("ef-adminlevel").textContent; + unmount(); + return level; + }; + + expect(adminLevelFor([{ role: "system", library: "nypl" }])).toBe("3"); + expect(adminLevelFor([{ role: "manager", library: "nypl" }])).toBe("2"); + expect(adminLevelFor([{ role: "librarian", library: "nypl" }])).toBe("1"); + }); +}); diff --git a/tests/jest/components/ProtocolFormField.test.tsx b/tests/jest/components/ProtocolFormField.test.tsx index c385488359..65f66e577c 100644 --- a/tests/jest/components/ProtocolFormField.test.tsx +++ b/tests/jest/components/ProtocolFormField.test.tsx @@ -1,15 +1,16 @@ import * as React from "react"; -import { render, screen, fireEvent, act } from "@testing-library/react"; +import { + render, + screen, + fireEvent, + act, + within, + waitFor, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import ProtocolFormField from "../../../src/components/ProtocolFormField"; import { MONOSPACE_FONT_STACK } from "../../../src/fonts"; -// NB: This file adds / duplicates existing tests from: -// - `src/components/__tests__/ProtocolFormField-test.tsx`. -// -// Those tests should eventually be migrated here and -// adapted to the Jest/React Testing Library paradigm. - describe("ProtocolFormField — json type", () => { const jsonSetting = { key: "config", @@ -126,3 +127,438 @@ describe("ProtocolFormField", () => { expect(input.value).toBe(testDate); }); }); + +// These render each setting `type` and assert the rendered control via the DOM, +// reading uncontrolled values through the component's real ref-based `getValue()` +// / `clear()` API (the same API production calls), like the json-type tests above. +describe("ProtocolFormField — setting types", () => { + const baseSetting = { + key: "setting", + label: "label", + description: "

description

", + }; + + const menuOptions = ["A", "B", "C"].map((x) => ( + + )); + + it("renders a text setting and reads/clears its value via ref", () => { + const ref = React.createRef(); + render( + + ); + const input = screen.getByLabelText("label") as HTMLInputElement; + expect(input.tagName).toBe("INPUT"); + expect(input.value).toBe("test"); + expect(ref.current!.getValue()).toBe("test"); + + act(() => { + ref.current!.clear(); + }); + expect(input.value).toBe(""); + }); + + it("shows an (Optional) prefix on the description of a non-required field", () => { + const { container } = render( + + ); + expect(container.querySelector(".description")).toHaveTextContent( + "(Optional) description" + ); + }); + + it("renders a setting with a default value", () => { + render( + + ); + expect((screen.getByLabelText("label") as HTMLInputElement).value).toBe( + "default" + ); + }); + + it("renders a number setting", () => { + render( + + ); + expect((screen.getByLabelText("label") as HTMLInputElement).value).toBe( + "42" + ); + }); + + it("renders a textarea setting", () => { + render( + + ); + const ta = screen.getByLabelText("label") as HTMLTextAreaElement; + expect(ta.tagName).toBe("TEXTAREA"); + expect(ta.value).toBe("test"); + }); + + it("renders a select setting with its options", () => { + render( + + ); + const select = screen.getByLabelText("label") as HTMLSelectElement; + const options = within(select).getAllByRole( + "option" + ) as HTMLOptionElement[]; + expect(options).toHaveLength(2); + expect(options[0].value).toBe("option1"); + expect(options[0]).toHaveTextContent("option 1"); + expect(options[1].value).toBe("option2"); + expect(options[1]).toHaveTextContent("option 2"); + }); + + it("renders a randomizable setting whose button generates a 32-char value", async () => { + const user = userEvent.setup(); + const ref = React.createRef(); + render( + + ); + const button = screen.getByRole("button", { name: /random/i }); + await user.click(button); + const value = ref.current!.getValue(); + expect(value).toHaveLength(32); + }); + + it("renders an image setting with a preview img", () => { + const { container } = render( + + ); + const img = container.querySelector("img"); + expect(img).not.toBeNull(); + expect(img!.getAttribute("src")).toBe("image data"); + }); + + it("renders a color-picker setting with its label and default value", () => { + const { container } = render( + + ); + expect(screen.getByText("label")).toBeInTheDocument(); + const hidden = container.querySelector( + 'input[type="hidden"]' + ) as HTMLInputElement; + expect(hidden.value).toBe("#aaaaaa"); + }); + + it("renders a list setting and reads its value via ref", () => { + const ref = React.createRef(); + render( + + ); + // The InputList renders an "Add" button. + expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument(); + expect(ref.current!.getValue()).toEqual(["item 1", "item 2"]); + }); + + it("optionally renders instructions for a list setting", () => { + const { container } = render( +
  • Step 1
  • ", + }} + disabled={false} + /> + ); + const instructions = container.querySelector(".well"); + expect(instructions).not.toBeNull(); + expect(instructions).toHaveClass("description"); + expect(instructions).toHaveTextContent("Step 1"); + }); + + it("renders a menu setting as a select", () => { + const ref = React.createRef(); + const { container } = render( + + ); + expect(container.querySelector("select")).not.toBeNull(); + expect(ref.current!.getValue()).toEqual([]); + // With no items selected, the InputList shows the alt text in their place. + expect(screen.getByText("Alternate")).toBeInTheDocument(); + }); + + it("renders a hidden setting inside an invisible wrapper and reads its value via ref", () => { + const ref = React.createRef(); + const { container } = render( + + ); + // The hidden element is wrapped in a visually-hidden div. + const wrapper = container.querySelector('div[style*="hidden"]'); + expect(wrapper).not.toBeNull(); + expect(ref.current!.getValue()).toBe("test"); + }); + + it("reads the value of a hidden list setting via ref", () => { + const ref = React.createRef(); + render( + + ); + expect(ref.current!.getValue()).toEqual(["item 1", "item 2"]); + }); + + it("resolves a hidden select value: first option, then default, then explicit value", () => { + const options = [ + { key: "option1", label: "option 1" }, + { key: "option2", label: "option 2" }, + { key: "option3", label: "option 3" }, + ]; + const selectValue = (extra: Record, value?: string) => { + const ref = React.createRef(); + render( + + ); + return ref.current!.getValue(); + }; + + // No value and no default -> the first option. + expect(selectValue({})).toBe("option1"); + // A default with no value -> the default. + expect(selectValue({ default: "option3" })).toBe("option3"); + // An explicit value -> that value, overriding the default. + expect(selectValue({ default: "option3" }, "option2")).toBe("option2"); + }); + + // Hidden settings render the same element inside an invisible wrapper, so each + // variant must still surface its value through its ref (elementRef for the + // scalar inputs, colorPickerRef for the color picker, inputListRef for the + // menu; hidden text and hidden list are covered above). + it.each([ + { + name: "date-picker", + extra: { type: "date-picker" }, + value: "2020-06-01", + }, + { name: "number", extra: { type: "number" }, value: "42" }, + { name: "textarea", extra: { type: "textarea" }, value: "some text" }, + { name: "color-picker", extra: { type: "color-picker" }, value: "#123456" }, + { name: "randomizable", extra: { randomizable: true }, value: "test" }, + { name: "menu", extra: { type: "menu", menuOptions }, value: ["A"] }, + ])( + "reads a hidden $name setting's value back via its ref", + ({ extra, value }) => { + const ref = React.createRef(); + render( + + ); + expect(ref.current!.getValue()).toEqual(value); + } + ); + + it("falls back to the setting's default for a hidden setting with no value", () => { + const ref = React.createRef(); + render( + + ); + expect(ref.current!.getValue()).toBe("the default"); + }); + + it("does not submit a stale value through a hidden image setting", () => { + const ref = React.createRef(); + const { container } = render( + + ); + // An image setting is a file input whose value is forced to undefined, so it + // submits nothing; the current value is surfaced as a preview instead. + expect(ref.current!.getValue()).toBe(""); + expect(container.querySelector("img")).toHaveAttribute( + "src", + "data:image/png;base64,abc" + ); + }); + + it("forwards onChange to the underlying EditableInput", async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + + ); + await user.type(screen.getByLabelText("label"), "x"); + expect(onChange).toHaveBeenCalled(); + }); + + it("forwards onChange to the InputList (a list setting reports add/remove)", async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + + ); + await user.type(screen.getByRole("textbox"), "new item"); + await user.click(screen.getByRole("button", { name: /add/i })); + await waitFor(() => expect(onChange).toHaveBeenCalled()); + }); + + it("forwards readOnly to the InputList (list item inputs are read-only)", () => { + render( + + ); + expect( + (screen.getByDisplayValue("item 1") as HTMLInputElement).readOnly + ).toBe(true); + }); + + // A `menu` setting also goes through InputList, but its add-control is built by + // InputList.renderMenu(), which does not restate onChange/readOnly — it relies + // on them arriving through createEditableInput. That is a different path from + // the `list` add-control above, so it needs its own coverage. + it("forwards onChange to the InputList menu control", async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + + ); + + await user.selectOptions(screen.getByRole("combobox"), "B"); + + expect(onChange).toHaveBeenCalled(); + }); + + it("forwards readOnly to the InputList menu control", () => { + const onChange = jest.fn(); + render( + + ); + + // A