@thepalaceproject/circulation-admin 1.43.0-post.11 → 1.43.0-post.14

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.
@@ -0,0 +1,458 @@
1
+ import * as React from "react";
2
+ import { render, screen, within, fireEvent } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import ClassificationsForm from "../../../src/components/ClassificationsForm";
6
+ import { BookData } from "../../../src/interfaces";
7
+ import genreData from "../../../src/components/__tests__/genreData";
8
+
9
+ // ClassificationsForm is a plain (unconnected) class component, so a bare
10
+ // `render` is enough — no store/context/router needed.
11
+
12
+ const noValuesBook = (): BookData => ({
13
+ id: "1",
14
+ title: "title",
15
+ audience: undefined,
16
+ targetAgeRange: ["12", "16"],
17
+ fiction: undefined,
18
+ categories: ["Space Opera"],
19
+ });
20
+
21
+ const fullBook = (): BookData => ({
22
+ id: "1",
23
+ title: "title",
24
+ audience: "Young Adult",
25
+ targetAgeRange: ["12", "16"],
26
+ fiction: true,
27
+ categories: ["Space Opera"],
28
+ });
29
+
30
+ type RenderProps = Partial<React.ComponentProps<typeof ClassificationsForm>>;
31
+
32
+ const renderForm = (book: BookData, props: RenderProps = {}) => {
33
+ const editClassifications = jest.fn();
34
+ const result = render(
35
+ <ClassificationsForm
36
+ book={book}
37
+ genreTree={genreData}
38
+ editClassifications={editClassifications}
39
+ {...props}
40
+ />
41
+ );
42
+ return { ...result, editClassifications };
43
+ };
44
+
45
+ const audienceSelect = (container: HTMLElement) =>
46
+ container.querySelector<HTMLSelectElement>("select[name='audience']");
47
+ const fictionRadio = (container: HTMLElement, value: string) =>
48
+ container.querySelector<HTMLInputElement>(
49
+ `input[name='fiction'][value='${value}']`
50
+ );
51
+ const removeButtons = (container: HTMLElement) =>
52
+ Array.from(container.querySelectorAll<HTMLElement>(".with-remove-button"));
53
+
54
+ // Selects a top-level genre in the GenreForm and clicks "Add".
55
+ const addGenre = async (container: HTMLElement, genre: string) => {
56
+ const user = userEvent.setup();
57
+ const option = container.querySelector<HTMLOptionElement>(
58
+ `select[name='genre'] option[value='${genre}']`
59
+ );
60
+ fireEvent.click(option);
61
+ await user.click(screen.getByRole("button", { name: "Add" }));
62
+ };
63
+
64
+ // Selects a genre + subgenre in the GenreForm and clicks "Add".
65
+ const addSubgenre = async (
66
+ container: HTMLElement,
67
+ genre: string,
68
+ subgenre: string
69
+ ) => {
70
+ const user = userEvent.setup();
71
+ fireEvent.click(
72
+ container.querySelector<HTMLOptionElement>(
73
+ `select[name='genre'] option[value='${genre}']`
74
+ )
75
+ );
76
+ fireEvent.click(
77
+ container.querySelector<HTMLOptionElement>(
78
+ `select[name='subgenre'] option[value='${subgenre}']`
79
+ )
80
+ );
81
+ await user.click(screen.getByRole("button", { name: "Add" }));
82
+ };
83
+
84
+ describe("ClassificationsForm", () => {
85
+ let confirmSpy: jest.SpyInstance;
86
+ let alertSpy: jest.SpyInstance;
87
+
88
+ beforeEach(() => {
89
+ confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true);
90
+ alertSpy = jest.spyOn(window, "alert").mockImplementation(() => undefined);
91
+ });
92
+
93
+ afterEach(() => {
94
+ jest.restoreAllMocks();
95
+ });
96
+
97
+ describe("rendering without classification values", () => {
98
+ it("shows no audience or fiction classification selected", () => {
99
+ const { container } = renderForm(noValuesBook());
100
+
101
+ const audience = audienceSelect(container);
102
+ expect(screen.getByText("Audience")).toBeInTheDocument();
103
+ expect(audience).toHaveValue("None");
104
+ // The "None" option is only present when no audience is selected.
105
+ expect(within(audience).getAllByRole("option")).toHaveLength(7);
106
+
107
+ const none = fictionRadio(container, "none");
108
+ const fiction = fictionRadio(container, "fiction");
109
+ const nonfiction = fictionRadio(container, "nonfiction");
110
+
111
+ expect(none).toHaveAttribute("type", "radio");
112
+ expect(none).toBeChecked();
113
+ expect(fiction).not.toBeChecked();
114
+ expect(nonfiction).not.toBeChecked();
115
+
116
+ const group = container.querySelector(".fiction-radio-input");
117
+ expect(group).toHaveTextContent("None");
118
+ expect(group).toHaveTextContent("Fiction");
119
+ expect(group).toHaveTextContent("Nonfiction");
120
+ });
121
+
122
+ it("does not submit without an audience or fiction classification", async () => {
123
+ const user = userEvent.setup();
124
+ const { container, editClassifications } = renderForm(noValuesBook());
125
+
126
+ await user.click(screen.getByRole("button", { name: "Save" }));
127
+
128
+ expect(editClassifications).not.toHaveBeenCalled();
129
+ expect(audienceSelect(container)).toHaveValue("None");
130
+ });
131
+
132
+ it("renders error messages without an audience or fiction classification", async () => {
133
+ const user = userEvent.setup();
134
+ const { container, editClassifications } = renderForm(noValuesBook());
135
+
136
+ await user.click(screen.getByRole("button", { name: "Save" }));
137
+
138
+ const alert = container.querySelector(".alert-danger");
139
+ expect(alert).toBeInTheDocument();
140
+ expect(alert).toHaveTextContent("No Audience classification selected.");
141
+ expect(alert).toHaveTextContent("No Fiction classification selected.");
142
+ expect(editClassifications).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it("moves focus to the error message so it is announced", () => {
146
+ jest.useFakeTimers();
147
+ // Watch the focus() call itself: the component focuses the alert on a
148
+ // 500ms timer. Use fireEvent (not userEvent, which focuses the target it
149
+ // clicks) so the only focus() we observe is the intended one.
150
+ const focusSpy = jest.spyOn(HTMLElement.prototype, "focus");
151
+ try {
152
+ const { container } = renderForm(noValuesBook());
153
+
154
+ fireEvent.click(screen.getByRole("button", { name: "Save" }));
155
+ expect(focusSpy).not.toHaveBeenCalled();
156
+
157
+ jest.advanceTimersByTime(500);
158
+
159
+ const alert = container.querySelector(".alert-danger");
160
+ expect(focusSpy).toHaveBeenCalledTimes(1);
161
+ expect(focusSpy.mock.instances[0]).toBe(alert);
162
+ } finally {
163
+ focusSpy.mockRestore();
164
+ jest.useRealTimers();
165
+ }
166
+ });
167
+
168
+ it("does not submit if you didn't select an audience", async () => {
169
+ const user = userEvent.setup();
170
+ const { container, editClassifications } = renderForm(noValuesBook());
171
+
172
+ // Choose a fiction classification but leave the audience unset.
173
+ await user.click(fictionRadio(container, "fiction"));
174
+ await user.click(screen.getByRole("button", { name: "Save" }));
175
+
176
+ expect(editClassifications).not.toHaveBeenCalled();
177
+ expect(container.querySelector(".alert-danger")).toBeInTheDocument();
178
+
179
+ // Now choose an audience and the form submits, clearing the error.
180
+ await user.selectOptions(audienceSelect(container), "Adult");
181
+ await user.click(screen.getByRole("button", { name: "Save" }));
182
+
183
+ expect(editClassifications).toHaveBeenCalledTimes(1);
184
+ expect(container.querySelector(".alert-danger")).not.toBeInTheDocument();
185
+ });
186
+
187
+ it("does not submit if you didn't select a fiction classification", async () => {
188
+ const user = userEvent.setup();
189
+ const { container, editClassifications } = renderForm(noValuesBook());
190
+
191
+ // Choose an audience but leave the fiction classification unset.
192
+ await user.selectOptions(audienceSelect(container), "Adult");
193
+ await user.click(screen.getByRole("button", { name: "Save" }));
194
+
195
+ expect(editClassifications).not.toHaveBeenCalled();
196
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
197
+
198
+ // Now choose a fiction classification and the form submits.
199
+ await user.click(fictionRadio(container, "nonfiction"));
200
+ await user.click(screen.getByRole("button", { name: "Save" }));
201
+
202
+ expect(editClassifications).toHaveBeenCalledTimes(1);
203
+ });
204
+ });
205
+
206
+ describe("rendering", () => {
207
+ it("shows an editable select with audience options", () => {
208
+ const { container } = renderForm(fullBook());
209
+
210
+ const audience = audienceSelect(container);
211
+ expect(screen.getByText("Audience")).toBeInTheDocument();
212
+ expect(audience).toHaveValue("Young Adult");
213
+ // The "None" option is not rendered once an audience is selected.
214
+ expect(within(audience).getAllByRole("option")).toHaveLength(6);
215
+ });
216
+
217
+ it("shows editable inputs with min and max target age", () => {
218
+ const { container } = renderForm(fullBook());
219
+
220
+ expect(
221
+ container.querySelector("input[name='target_age_min']")
222
+ ).toHaveValue("12");
223
+ expect(
224
+ container.querySelector("input[name='target_age_max']")
225
+ ).toHaveValue("16");
226
+ });
227
+
228
+ it("shows editable radio buttons with fiction status", () => {
229
+ const { container } = renderForm(fullBook());
230
+
231
+ // With a fiction value set, the "None" fiction radio is not rendered.
232
+ expect(fictionRadio(container, "none")).toBeNull();
233
+ expect(fictionRadio(container, "fiction")).toBeChecked();
234
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
235
+
236
+ const group = container.querySelector(".fiction-radio-input");
237
+ expect(group).toHaveTextContent("Fiction");
238
+ expect(group).toHaveTextContent("Nonfiction");
239
+ });
240
+
241
+ it("shows the book's full genres with remove buttons", () => {
242
+ const { container } = renderForm(fullBook());
243
+
244
+ const buttons = removeButtons(container);
245
+ expect(buttons).toHaveLength(1);
246
+ expect(buttons[0]).toHaveTextContent("Science Fiction > Space Opera");
247
+ expect(
248
+ within(buttons[0]).getByRole("button", { name: /Delete/ })
249
+ ).toBeInTheDocument();
250
+ });
251
+
252
+ it("shows the book's full genres even if inconsistent with fiction status", () => {
253
+ const editClassifications = jest.fn();
254
+ const { container, rerender } = render(
255
+ <ClassificationsForm
256
+ book={fullBook()}
257
+ genreTree={genreData}
258
+ editClassifications={editClassifications}
259
+ />
260
+ );
261
+
262
+ rerender(
263
+ <ClassificationsForm
264
+ book={{ ...fullBook(), fiction: false }}
265
+ genreTree={genreData}
266
+ editClassifications={editClassifications}
267
+ />
268
+ );
269
+
270
+ const buttons = removeButtons(container);
271
+ expect(buttons).toHaveLength(1);
272
+ expect(buttons[0]).toHaveTextContent("Science Fiction > Space Opera");
273
+ });
274
+
275
+ it("shows a genre form", () => {
276
+ const { container } = renderForm(fullBook());
277
+
278
+ const genreSelect = container.querySelector("select[name='genre']");
279
+ expect(genreSelect).toBeInTheDocument();
280
+ expect(genreSelect).not.toBeDisabled();
281
+ expect(screen.getByText("Add Genre")).toBeInTheDocument();
282
+ });
283
+
284
+ it("shows a submit button", () => {
285
+ renderForm(fullBook());
286
+ expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
287
+ });
288
+ });
289
+
290
+ describe("behavior", () => {
291
+ it("shows and hides target age inputs when audience changes", async () => {
292
+ const user = userEvent.setup();
293
+ const { container } = renderForm(fullBook());
294
+
295
+ expect(
296
+ container.querySelector("input[name='target_age_min']")
297
+ ).toBeInTheDocument();
298
+ expect(
299
+ container.querySelector("input[name='target_age_max']")
300
+ ).toBeInTheDocument();
301
+
302
+ await user.selectOptions(audienceSelect(container), "Adult");
303
+ expect(
304
+ container.querySelector("input[name='target_age_min']")
305
+ ).not.toBeInTheDocument();
306
+ expect(
307
+ container.querySelector("input[name='target_age_max']")
308
+ ).not.toBeInTheDocument();
309
+
310
+ await user.selectOptions(audienceSelect(container), "Children");
311
+ expect(
312
+ container.querySelector("input[name='target_age_min']")
313
+ ).toBeInTheDocument();
314
+ expect(
315
+ container.querySelector("input[name='target_age_max']")
316
+ ).toBeInTheDocument();
317
+ });
318
+
319
+ it("changes both fiction status radio buttons", async () => {
320
+ const user = userEvent.setup();
321
+ const { container } = renderForm(fullBook());
322
+
323
+ expect(fictionRadio(container, "fiction")).toBeChecked();
324
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
325
+
326
+ // Switching to nonfiction prompts for confirmation and clears the
327
+ // (fiction) genre.
328
+ await user.click(fictionRadio(container, "nonfiction"));
329
+ expect(confirmSpy).toHaveBeenCalled();
330
+ expect(removeButtons(container)).toHaveLength(0);
331
+ expect(fictionRadio(container, "fiction")).not.toBeChecked();
332
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
333
+
334
+ await user.click(fictionRadio(container, "fiction"));
335
+ expect(fictionRadio(container, "fiction")).toBeChecked();
336
+ expect(fictionRadio(container, "nonfiction")).not.toBeChecked();
337
+ });
338
+
339
+ it("adds a genre only after validating it against the audience", async () => {
340
+ const { container } = renderForm(fullBook());
341
+
342
+ expect(removeButtons(container)).toHaveLength(1);
343
+
344
+ // Erotica is not allowed for a Young Adult audience: it is rejected with
345
+ // an alert and not added.
346
+ await addGenre(container, "Erotica");
347
+ expect(alertSpy).toHaveBeenCalled();
348
+ let buttons = removeButtons(container);
349
+ expect(buttons).toHaveLength(1);
350
+ expect(buttons[0]).not.toHaveTextContent("Erotica");
351
+
352
+ // Folklore is allowed and gets added.
353
+ await addGenre(container, "Folklore");
354
+ buttons = removeButtons(container);
355
+ expect(buttons).toHaveLength(2);
356
+ expect(buttons.some((b) => b.textContent.includes("Folklore"))).toBe(
357
+ true
358
+ );
359
+ });
360
+
361
+ it("removes a genre when its remove button is clicked", async () => {
362
+ const user = userEvent.setup();
363
+ const { container } = renderForm(fullBook());
364
+
365
+ const button = removeButtons(container)[0];
366
+ await user.click(within(button).getByRole("button", { name: /Delete/ }));
367
+
368
+ expect(removeButtons(container)).toHaveLength(0);
369
+ });
370
+
371
+ it("submits data when the submit button is clicked", async () => {
372
+ const user = userEvent.setup();
373
+ const { editClassifications } = renderForm(fullBook());
374
+
375
+ await user.click(screen.getByRole("button", { name: "Save" }));
376
+
377
+ expect(editClassifications).toHaveBeenCalledTimes(1);
378
+ const data = editClassifications.mock.calls[0][0] as FormData;
379
+ expect(data.get("audience")).toBe("Young Adult");
380
+ expect(data.get("target_age_min")).toBe("12");
381
+ expect(data.get("target_age_max")).toBe("16");
382
+ expect(data.get("fiction")).toBe("fiction");
383
+ expect(data.getAll("genres")).toEqual(["Space Opera"]);
384
+ });
385
+
386
+ it("updates state upon receiving new state-related props", () => {
387
+ const editClassifications = jest.fn();
388
+ const { container, rerender } = render(
389
+ <ClassificationsForm
390
+ book={fullBook()}
391
+ genreTree={genreData}
392
+ editClassifications={editClassifications}
393
+ />
394
+ );
395
+
396
+ rerender(
397
+ <ClassificationsForm
398
+ book={{
399
+ ...fullBook(),
400
+ audience: "Adult",
401
+ fiction: false,
402
+ categories: ["Cooking"],
403
+ }}
404
+ genreTree={genreData}
405
+ editClassifications={editClassifications}
406
+ />
407
+ );
408
+
409
+ expect(audienceSelect(container)).toHaveValue("Adult");
410
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
411
+ expect(fictionRadio(container, "fiction")).not.toBeChecked();
412
+
413
+ const buttons = removeButtons(container);
414
+ expect(buttons).toHaveLength(1);
415
+ expect(buttons[0]).toHaveTextContent("Food & Health > Cooking");
416
+ });
417
+
418
+ it("doesn't reset user edits upon receiving new state-unrelated props", async () => {
419
+ const user = userEvent.setup();
420
+ const editClassifications = jest.fn();
421
+ const { container, rerender } = render(
422
+ <ClassificationsForm
423
+ book={fullBook()}
424
+ genreTree={genreData}
425
+ editClassifications={editClassifications}
426
+ />
427
+ );
428
+
429
+ // The user switches to nonfiction (clearing the fiction genre) and adds a
430
+ // nonfiction genre.
431
+ await user.click(fictionRadio(container, "nonfiction"));
432
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
433
+ expect(removeButtons(container)).toHaveLength(0);
434
+
435
+ await addSubgenre(container, "Food & Health", "Cooking");
436
+ expect(removeButtons(container)).toHaveLength(1);
437
+ expect(container.querySelector(".with-remove-button")).toHaveTextContent(
438
+ "Food & Health > Cooking"
439
+ );
440
+
441
+ // A re-render caused by an unrelated prop change (disabling the form on
442
+ // submit) must not discard those edits.
443
+ rerender(
444
+ <ClassificationsForm
445
+ book={fullBook()}
446
+ genreTree={genreData}
447
+ editClassifications={editClassifications}
448
+ disabled={true}
449
+ />
450
+ );
451
+
452
+ expect(fictionRadio(container, "nonfiction")).toBeChecked();
453
+ expect(container.querySelector(".with-remove-button")).toHaveTextContent(
454
+ "Food & Health > Cooking"
455
+ );
456
+ });
457
+ });
458
+ });
@@ -0,0 +1,157 @@
1
+ import * as React from "react";
2
+ import { render, screen, within } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import Contributors from "../../../src/components/Contributors";
6
+ import { RolesData, ContributorData } from "../../../src/interfaces";
7
+
8
+ const roles: RolesData = {
9
+ aut: "Author",
10
+ ill: "Illustrator",
11
+ nrt: "Narrator",
12
+ trl: "Translator",
13
+ };
14
+
15
+ const contributors: ContributorData[] = [
16
+ { name: "A Translator", role: "trl" },
17
+ { name: "A Narrator", role: "nrt" },
18
+ ];
19
+
20
+ const renderContributors = (
21
+ props: Partial<React.ComponentProps<typeof Contributors>> = {}
22
+ ) =>
23
+ render(
24
+ <Contributors
25
+ roles={roles}
26
+ contributors={contributors}
27
+ disabled={false}
28
+ {...props}
29
+ />
30
+ );
31
+
32
+ const existingRows = (container: HTMLElement) =>
33
+ Array.from(container.querySelectorAll<HTMLElement>(".with-remove-button"));
34
+
35
+ const newContributorForm = () =>
36
+ screen
37
+ .getByRole("button", { name: "Add" })
38
+ .closest(".contributor-form") as HTMLElement;
39
+
40
+ const roleSelect = (row: HTMLElement) =>
41
+ row.querySelector('select[name="contributor-role"]') as HTMLSelectElement;
42
+ const nameInput = (row: HTMLElement) =>
43
+ row.querySelector('input[name="contributor-name"]') as HTMLInputElement;
44
+
45
+ describe("Contributors", () => {
46
+ it("displays a label", () => {
47
+ renderContributors();
48
+ expect(screen.getByText("Authors and Contributors")).toBeInTheDocument();
49
+ });
50
+
51
+ it("displays a list of existing contributors", () => {
52
+ const { container } = renderContributors();
53
+ const rows = existingRows(container);
54
+ expect(rows).toHaveLength(2);
55
+
56
+ expect(roleSelect(rows[0])).toHaveValue("Translator");
57
+ expect(nameInput(rows[0])).toHaveValue("A Translator");
58
+ expect(roleSelect(rows[1])).toHaveValue("Narrator");
59
+ expect(nameInput(rows[1])).toHaveValue("A Narrator");
60
+
61
+ // Each role menu offers the full set of roles.
62
+ const options = within(roleSelect(rows[0]))
63
+ .getAllByRole("option")
64
+ .map((o) => (o as HTMLOptionElement).value);
65
+ expect(options).toStrictEqual(Object.values(roles));
66
+ });
67
+
68
+ it("displays a blank field for adding a new contributor", () => {
69
+ renderContributors();
70
+ const form = newContributorForm();
71
+ expect(roleSelect(form)).toHaveValue("Author");
72
+ expect(nameInput(form)).toHaveValue("");
73
+
74
+ const button = within(form).getByRole("button", { name: "Add" });
75
+ expect(button).toHaveClass("add-contributor");
76
+ });
77
+
78
+ it("deletes a contributor", async () => {
79
+ const user = userEvent.setup();
80
+ const { container } = renderContributors();
81
+
82
+ let rows = existingRows(container);
83
+ expect(rows).toHaveLength(2);
84
+
85
+ // Remove the first (Translator) row.
86
+ await user.click(within(rows[0]).getByRole("button"));
87
+
88
+ rows = existingRows(container);
89
+ expect(rows).toHaveLength(1);
90
+ expect(roleSelect(rows[0])).toHaveValue("Narrator");
91
+ expect(nameInput(rows[0])).toHaveValue("A Narrator");
92
+ });
93
+
94
+ it("adds a contributor", async () => {
95
+ const user = userEvent.setup();
96
+ const { container } = renderContributors();
97
+ expect(existingRows(container)).toHaveLength(2);
98
+
99
+ const form = newContributorForm();
100
+ await user.selectOptions(roleSelect(form), "Illustrator");
101
+ await user.type(nameInput(form), "An Illustrator");
102
+ await user.click(within(form).getByRole("button", { name: "Add" }));
103
+
104
+ const rows = existingRows(container);
105
+ expect(rows).toHaveLength(3);
106
+ expect(roleSelect(rows[2])).toHaveValue("Illustrator");
107
+ expect(nameInput(rows[2])).toHaveValue("An Illustrator");
108
+ });
109
+
110
+ it("does not add an empty string as a contributor's name", async () => {
111
+ const user = userEvent.setup();
112
+ const { container } = renderContributors();
113
+
114
+ // The Add button starts disabled, so clicking it does nothing.
115
+ expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
116
+ await user.click(screen.getByRole("button", { name: "Add" }));
117
+ expect(existingRows(container)).toHaveLength(2);
118
+
119
+ // Typing a name enables the button and lets us add.
120
+ await user.type(nameInput(newContributorForm()), "An Author");
121
+ expect(screen.getByRole("button", { name: "Add" })).toBeEnabled();
122
+ await user.click(screen.getByRole("button", { name: "Add" }));
123
+ expect(existingRows(container)).toHaveLength(3);
124
+
125
+ // Clearing the name disables the button again.
126
+ await user.clear(nameInput(newContributorForm()));
127
+ expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
128
+ await user.click(screen.getByRole("button", { name: "Add" }));
129
+ expect(existingRows(container)).toHaveLength(3);
130
+ });
131
+
132
+ it("shows the full name of a contributor's known role", () => {
133
+ // "trl" and "nrt" resolve to their full names in the role menus.
134
+ const { container } = renderContributors();
135
+ const rows = existingRows(container);
136
+ expect(roleSelect(rows[0])).toHaveValue("Translator");
137
+ expect(roleSelect(rows[1])).toHaveValue("Narrator");
138
+ });
139
+
140
+ it("resets the form after adding a contributor", async () => {
141
+ const user = userEvent.setup();
142
+ renderContributors();
143
+
144
+ const form = newContributorForm();
145
+ await user.selectOptions(roleSelect(form), "Illustrator");
146
+ await user.type(nameInput(form), "An Illustrator");
147
+ await user.click(within(form).getByRole("button", { name: "Add" }));
148
+
149
+ // The add form is blank again, defaulted to "Author", with a disabled button.
150
+ const resetForm = newContributorForm();
151
+ expect(nameInput(resetForm)).toHaveValue("");
152
+ expect(roleSelect(resetForm)).toHaveValue("Author");
153
+ expect(
154
+ within(resetForm).getByRole("button", { name: "Add" })
155
+ ).toBeDisabled();
156
+ });
157
+ });