@thepalaceproject/circulation-admin 1.43.0-post.5 → 1.43.0-post.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -143,5 +143,5 @@
143
143
  "*.{js,jsx,ts,tsx,css,md}": "prettier --write",
144
144
  "*.{js,css,md}": "prettier --write"
145
145
  },
146
- "version": "1.43.0-post.5"
146
+ "version": "1.43.0-post.7"
147
147
  }
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import SaveButton from "../../../src/components/SaveButton";
6
+
7
+ describe("SaveButton", () => {
8
+ it("says 'Save' when given no text", () => {
9
+ render(<SaveButton disabled={false} submit={jest.fn()} />);
10
+
11
+ expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
12
+ });
13
+
14
+ it("displays the text it is given", () => {
15
+ render(
16
+ <SaveButton
17
+ disabled={false}
18
+ submit={jest.fn()}
19
+ text="some other string"
20
+ />
21
+ );
22
+
23
+ expect(
24
+ screen.getByRole("button", { name: "some other string" })
25
+ ).toBeInTheDocument();
26
+ });
27
+
28
+ it("submits when clicked", async () => {
29
+ const user = userEvent.setup();
30
+ const submit = jest.fn();
31
+ render(<SaveButton disabled={false} submit={submit} />);
32
+
33
+ await user.click(screen.getByRole("button"));
34
+
35
+ expect(submit).toHaveBeenCalledTimes(1);
36
+ });
37
+
38
+ it("does nothing when disabled", async () => {
39
+ const user = userEvent.setup();
40
+ const submit = jest.fn();
41
+ render(<SaveButton disabled={true} submit={submit} />);
42
+
43
+ await user.click(screen.getByRole("button"));
44
+
45
+ expect(submit).not.toHaveBeenCalled();
46
+ });
47
+ });
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ import UpdatingLoader from "../../../src/components/UpdatingLoader";
5
+
6
+ describe("UpdatingLoader", () => {
7
+ it("renders only its container when not showing", () => {
8
+ const { container } = render(<UpdatingLoader show={false} />);
9
+
10
+ expect(
11
+ container.querySelector(".updating-loader-container")
12
+ ).toBeInTheDocument();
13
+ expect(container.querySelector(".updating-loader")).not.toBeInTheDocument();
14
+ expect(screen.queryByText("Updating")).not.toBeInTheDocument();
15
+ });
16
+
17
+ it("renders the default message when showing", () => {
18
+ render(<UpdatingLoader show={true} />);
19
+
20
+ expect(screen.getByText("Updating")).toBeInTheDocument();
21
+ });
22
+
23
+ it("renders the text it is given instead of the default", () => {
24
+ render(<UpdatingLoader show={true} text="Doing something" />);
25
+
26
+ expect(screen.getByText("Doing something")).toBeInTheDocument();
27
+ expect(screen.queryByText("Updating")).not.toBeInTheDocument();
28
+ });
29
+
30
+ it("renders a spinner", () => {
31
+ const { container } = render(<UpdatingLoader show={true} />);
32
+
33
+ // The spinner is a Font Awesome <i>, which carries no accessible role.
34
+ expect(container.querySelector("i.fa-spinner")).toBeInTheDocument();
35
+ });
36
+ });
@@ -0,0 +1,135 @@
1
+ import { FormDataShim } from "./formDataShim";
2
+
3
+ // Verifies the shim mirrors the browser's `new FormData(form)` "successful
4
+ // controls" rules — the fidelity that makes the payload assertions in the form
5
+ // suites (BookEditForm, SitewideAnnouncements, ...) trustworthy.
6
+ describe("FormDataShim", () => {
7
+ const formWith = (html: string): HTMLFormElement => {
8
+ const form = document.createElement("form");
9
+ form.innerHTML = html;
10
+ return form;
11
+ };
12
+
13
+ it("includes named, enabled text controls", () => {
14
+ const data = new FormDataShim(
15
+ formWith(`<input name="title" value="Dune" />`)
16
+ );
17
+ expect(data.get("title")).toBe("Dune");
18
+ });
19
+
20
+ it("skips disabled controls, as the browser does", () => {
21
+ const data = new FormDataShim(
22
+ formWith(`<input name="title" value="Dune" disabled />`)
23
+ );
24
+ expect(data.get("title")).toBeNull();
25
+ });
26
+
27
+ it("skips unnamed controls", () => {
28
+ const data = new FormDataShim(formWith(`<input value="anon" />`));
29
+ expect(data.get("")).toBeNull();
30
+ });
31
+
32
+ it("skips controls disabled by an ancestor fieldset", () => {
33
+ // A control inside <fieldset disabled> is a non-successful control even
34
+ // though its own `disabled` property is false. (Attached to the document so
35
+ // the :disabled match resolves ancestry.)
36
+ const form = formWith(
37
+ `<fieldset disabled><input name="inside" value="x" /></fieldset>
38
+ <input name="outside" value="y" />`
39
+ );
40
+ document.body.appendChild(form);
41
+ try {
42
+ const data = new FormDataShim(form);
43
+ expect(data.get("inside")).toBeNull();
44
+ expect(data.get("outside")).toBe("y");
45
+ } finally {
46
+ form.remove();
47
+ }
48
+ });
49
+
50
+ it("includes a checkbox only when it is checked", () => {
51
+ expect(
52
+ new FormDataShim(
53
+ formWith(`<input type="checkbox" name="agree" value="yes" />`)
54
+ ).get("agree")
55
+ ).toBeNull();
56
+ expect(
57
+ new FormDataShim(
58
+ formWith(`<input type="checkbox" name="agree" value="yes" checked />`)
59
+ ).get("agree")
60
+ ).toBe("yes");
61
+ });
62
+
63
+ it("includes only the checked radio in a group", () => {
64
+ const data = new FormDataShim(
65
+ formWith(
66
+ `<input type="radio" name="size" value="s" />
67
+ <input type="radio" name="size" value="m" checked />`
68
+ )
69
+ );
70
+ expect(data.get("size")).toBe("m");
71
+ });
72
+
73
+ it("excludes submit/reset/button inputs", () => {
74
+ const data = new FormDataShim(
75
+ formWith(`<input type="submit" name="go" value="Save" />`)
76
+ );
77
+ expect(data.get("go")).toBeNull();
78
+ });
79
+
80
+ it("omits file inputs (the browser submits them as File objects)", () => {
81
+ // Deliberate divergence from real FormData: a value-reading shim can't
82
+ // reproduce a File, so file fields are treated as absent rather than sent
83
+ // as their string value. Pinned so a regression can't silently re-add them.
84
+ const data = new FormDataShim(formWith(`<input type="file" name="doc" />`));
85
+ expect(data.get("doc")).toBeNull();
86
+ });
87
+
88
+ it("includes every selected option of a multi-select", () => {
89
+ const form = document.createElement("form");
90
+ const select = document.createElement("select");
91
+ select.name = "langs";
92
+ select.multiple = true;
93
+ (
94
+ [
95
+ ["en", true],
96
+ ["fr", false],
97
+ ["de", true],
98
+ ] as const
99
+ ).forEach(([value, selected]) => {
100
+ const option = document.createElement("option");
101
+ option.value = value;
102
+ option.selected = selected;
103
+ select.appendChild(option);
104
+ });
105
+ form.appendChild(select);
106
+
107
+ expect(new FormDataShim(form).getAll("langs")).toStrictEqual(["en", "de"]);
108
+ });
109
+
110
+ it("supports the standard read/write surface", () => {
111
+ const data = new FormDataShim();
112
+ data.append("k", "1");
113
+ data.append("k", "2");
114
+ expect(data.getAll("k")).toStrictEqual(["1", "2"]);
115
+ expect(data.has("k")).toBe(true);
116
+ data.set("k", "3");
117
+ expect(data.getAll("k")).toStrictEqual(["3"]);
118
+ data.delete("k");
119
+ expect(data.has("k")).toBe(false);
120
+ });
121
+
122
+ it("set() replaces the first entry in place and drops later duplicates", () => {
123
+ const data = new FormDataShim();
124
+ data.append("a", "1");
125
+ data.append("b", "2");
126
+ data.append("a", "3");
127
+
128
+ data.set("a", "9");
129
+
130
+ expect(Array.from(data.entries())).toStrictEqual([
131
+ ["a", "9"],
132
+ ["b", "2"],
133
+ ]);
134
+ });
135
+ });
@@ -0,0 +1,112 @@
1
+ // The unit jsdom environment installs undici's `FormData`, which throws on
2
+ // `new FormData(formElement)`. The reusable-components `Form` builds FormData
3
+ // that way on submit, so tests that assert a submitted payload need a stand-in.
4
+ //
5
+ // This shim mirrors the browser's `new FormData(form)`: it serializes only
6
+ // "successful controls" — named, non-disabled controls, with checkboxes and
7
+ // radios contributing only when checked, submit/reset/button/image/file inputs
8
+ // excluded, and every selected option of a multi-select included. A naive shim
9
+ // that records *every* named control can make a payload assertion pass while the
10
+ // real browser omits a disabled or unchecked field.
11
+ export class FormDataShim {
12
+ private items: Array<[string, string]> = [];
13
+
14
+ constructor(form?: HTMLFormElement) {
15
+ if (!form) {
16
+ return;
17
+ }
18
+ // Serializes the form's own descendant controls; the forms under test don't
19
+ // associate external controls via `form="id"`, so those are not collected.
20
+ form
21
+ .querySelectorAll<
22
+ HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
23
+ >("input, select, textarea")
24
+ .forEach((el) => {
25
+ // `:disabled` (not the `.disabled` property) so controls disabled via an
26
+ // ancestor `<fieldset disabled>` are omitted too, as the browser does.
27
+ if (!el.name || el.matches(":disabled")) {
28
+ return;
29
+ }
30
+ if (el instanceof HTMLInputElement) {
31
+ // Buttons never contribute a value. File inputs are omitted
32
+ // deliberately: the browser submits them as File objects, which a
33
+ // value-reading shim can't reproduce, so tests treat them as absent.
34
+ if (
35
+ ["submit", "reset", "button", "image", "file"].includes(el.type)
36
+ ) {
37
+ return;
38
+ }
39
+ if ((el.type === "checkbox" || el.type === "radio") && !el.checked) {
40
+ return;
41
+ }
42
+ }
43
+ if (el instanceof HTMLSelectElement && el.multiple) {
44
+ Array.from(el.selectedOptions).forEach((option) =>
45
+ this.items.push([el.name, option.value])
46
+ );
47
+ return;
48
+ }
49
+ this.items.push([el.name, el.value]);
50
+ });
51
+ }
52
+
53
+ get(key: string) {
54
+ const found = this.items.find(([k]) => k === key);
55
+ return found ? found[1] : null;
56
+ }
57
+ getAll(key: string) {
58
+ return this.items.filter(([k]) => k === key).map(([, v]) => v);
59
+ }
60
+ has(key: string) {
61
+ return this.items.some(([k]) => k === key);
62
+ }
63
+ append(key: string, value: string) {
64
+ this.items.push([key, value]);
65
+ }
66
+ set(key: string, value: string) {
67
+ // Replace the first matching entry in place and drop any later duplicates,
68
+ // preserving position as FormData.set does.
69
+ const firstIndex = this.items.findIndex(([k]) => k === key);
70
+ this.items = this.items.filter(([k]) => k !== key);
71
+ if (firstIndex === -1) {
72
+ this.items.push([key, value]);
73
+ } else {
74
+ this.items.splice(firstIndex, 0, [key, value]);
75
+ }
76
+ }
77
+ delete(key: string) {
78
+ this.items = this.items.filter(([k]) => k !== key);
79
+ }
80
+ forEach(
81
+ callback: (value: string, key: string, parent: FormDataShim) => void
82
+ ) {
83
+ this.items.forEach(([k, v]) => callback(v, k, this));
84
+ }
85
+ keys() {
86
+ return this.items.map(([k]) => k)[Symbol.iterator]();
87
+ }
88
+ values() {
89
+ return this.items.map(([, v]) => v)[Symbol.iterator]();
90
+ }
91
+ entries() {
92
+ return this.items
93
+ .map(([k, v]) => [k, v] as [string, string])
94
+ [Symbol.iterator]();
95
+ }
96
+ [Symbol.iterator]() {
97
+ return this.entries();
98
+ }
99
+ }
100
+
101
+ // Swaps `window.FormData` for the shim around a suite, restoring it afterward.
102
+ // Call once at the top level of a test file.
103
+ export function installFormDataShim(): void {
104
+ let originalFormData: typeof FormData;
105
+ beforeAll(() => {
106
+ originalFormData = window.FormData;
107
+ (window as any).FormData = FormDataShim;
108
+ });
109
+ afterAll(() => {
110
+ (window as any).FormData = originalFormData;
111
+ });
112
+ }