@thepalaceproject/circulation-admin 1.43.0-post.18 → 1.43.0-post.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +22 -10
- package/README.md +11 -5
- package/dist/circulation-admin.js +1 -1
- package/jest.config.js +11 -44
- package/package.json +1 -5
- package/tests/jest/components/BookEditor.test.tsx +20 -21
- package/tests/jest/components/Classifications.test.tsx +2 -2
- package/tests/jest/components/ClassificationsForm.test.tsx +1 -1
- package/tests/jest/components/ClassificationsTable.test.tsx +1 -1
- package/tests/jest/components/CustomListsForBook.test.tsx +10 -5
- package/tests/jest/components/GenreForm.test.tsx +1 -1
- package/tests/jest/components/Lane.test.tsx +2 -3
- package/tests/jest/components/LaneEditor.test.tsx +10 -7
- package/tests/jest/components/Stats.test.tsx +0 -7
- package/tests/jest/features/book.test.ts +75 -80
- package/tests/jest/fixtures/classificationsData.ts +45 -0
- package/tests/jest/fixtures/genreData.ts +811 -0
- package/tests/jest/README.md +0 -6
- package/tests/jest/legacy-setup.ts +0 -13
- package/tests/jest/legacyJsdomEnvironment.js +0 -49
package/jest.config.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
2
|
-
|
|
3
|
-
// Options shared by both projects.
|
|
4
|
-
const sharedProjectConfig = {
|
|
2
|
+
module.exports = {
|
|
5
3
|
preset: "ts-jest",
|
|
4
|
+
// `jest-fixed-jsdom` restores Node's fetch/Response globals so MSW can
|
|
5
|
+
// intercept them.
|
|
6
|
+
testEnvironment: "jest-fixed-jsdom",
|
|
6
7
|
testEnvironmentOptions: {
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// MSW / jest-fixed-jsdom rely on the default resolution conditions; the empty
|
|
9
|
+
// string keeps jsdom's "browser" condition from pulling in ESM-only builds
|
|
10
|
+
// that ts-jest would not transform.
|
|
10
11
|
customExportConditions: [""],
|
|
11
12
|
},
|
|
12
13
|
setupFiles: ["<rootDir>/jest.polyfills.js"],
|
|
14
|
+
setupFilesAfterEnv: ["<rootDir>/tests/jest/jest-setup.ts"],
|
|
13
15
|
moduleNameMapper: {
|
|
14
16
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
|
15
17
|
"<rootDir>/tests/__mocks__/fileMock.js",
|
|
@@ -20,44 +22,9 @@ const sharedProjectConfig = {
|
|
|
20
22
|
// files that no test imports — otherwise they are silently omitted from the
|
|
21
23
|
// report rather than counted as uncovered, and the coverage percentage rises
|
|
22
24
|
// while real coverage is unchanged.
|
|
23
|
-
roots: ["<rootDir>/src"],
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
module.exports = {
|
|
25
|
+
roots: ["<rootDir>/src", "<rootDir>/tests/jest"],
|
|
26
|
+
testMatch: ["<rootDir>/tests/jest/**/*.test.{js,jsx,ts,tsx}"],
|
|
27
27
|
coverageDirectory: "coverage/jest",
|
|
28
28
|
coverageReporters: ["lcov", "text-summary"],
|
|
29
|
-
|
|
30
|
-
// `collectCoverageFrom` off the *global* config. Set it only per-project and it
|
|
31
|
-
// is silently undefined globally, so every executed file gets instrumented —
|
|
32
|
-
// including test helpers and everything under `tests/`.
|
|
33
|
-
collectCoverageFrom: [
|
|
34
|
-
"src/**/*.{ts,tsx}",
|
|
35
|
-
"!src/**/__tests__/**",
|
|
36
|
-
"!src/**/*.d.ts",
|
|
37
|
-
],
|
|
38
|
-
projects: [
|
|
39
|
-
{
|
|
40
|
-
...sharedProjectConfig,
|
|
41
|
-
displayName: "unit",
|
|
42
|
-
roots: [...sharedProjectConfig.roots, "<rootDir>/tests/jest"],
|
|
43
|
-
testMatch: ["<rootDir>/tests/jest/**/*.test.{js,jsx,ts,tsx}"],
|
|
44
|
-
// `jest-fixed-jsdom` restores Node's fetch/Response globals so MSW can
|
|
45
|
-
// intercept them.
|
|
46
|
-
testEnvironment: "jest-fixed-jsdom",
|
|
47
|
-
setupFilesAfterEnv: ["<rootDir>/tests/jest/jest-setup.ts"],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
// Legacy enzyme + chai + sinon tests, still in `src/**/__tests__/`. They are
|
|
51
|
-
// being rewritten as React Testing Library tests under `tests/jest/`; when the
|
|
52
|
-
// last one is gone, delete this project, its environment, and its setup file.
|
|
53
|
-
//
|
|
54
|
-
// These cannot use `jest-fixed-jsdom`: it swaps in undici's `FormData`, which
|
|
55
|
-
// rejects `new FormData(formElement)` and crashes any test that submits a form.
|
|
56
|
-
...sharedProjectConfig,
|
|
57
|
-
displayName: "legacy",
|
|
58
|
-
testMatch: ["<rootDir>/src/**/__tests__/*-test.{ts,tsx}"],
|
|
59
|
-
testEnvironment: "<rootDir>/tests/jest/legacyJsdomEnvironment.js",
|
|
60
|
-
setupFilesAfterEnv: ["<rootDir>/tests/jest/legacy-setup.ts"],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
29
|
+
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts"],
|
|
63
30
|
};
|
package/package.json
CHANGED
|
@@ -84,13 +84,10 @@
|
|
|
84
84
|
"@types/react-router": "^3.0.28",
|
|
85
85
|
"@types/recharts": "^1.8.28",
|
|
86
86
|
"@types/redux-mock-store": "^1.0.3",
|
|
87
|
-
"chai": "4.2.0",
|
|
88
87
|
"clean-webpack-plugin": "^2.0.1",
|
|
89
88
|
"copy-webpack-plugin": "14.0.0",
|
|
90
89
|
"css-loader": "^7.1.4",
|
|
91
90
|
"dotenv-cli": "^11.0.0",
|
|
92
|
-
"enzyme": "^3.11.0",
|
|
93
|
-
"enzyme-adapter-react-16": "^1.15.6",
|
|
94
91
|
"eslint": "^9.39.4",
|
|
95
92
|
"eslint-config-prettier": "^10.1.8",
|
|
96
93
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
@@ -119,7 +116,6 @@
|
|
|
119
116
|
"sass-lint": "^1.13.1",
|
|
120
117
|
"sass-loader": "^17.0.0",
|
|
121
118
|
"selenium-standalone": "^10.0.2",
|
|
122
|
-
"sinon": "22.0.0",
|
|
123
119
|
"style-loader": "^4.0.0",
|
|
124
120
|
"terser-webpack-plugin": "^5.3.6",
|
|
125
121
|
"ts-jest": "^29.2.6",
|
|
@@ -143,5 +139,5 @@
|
|
|
143
139
|
"*.{js,jsx,ts,tsx,css,md}": "prettier --write",
|
|
144
140
|
"*.{js,css,md}": "prettier --write"
|
|
145
141
|
},
|
|
146
|
-
"version": "1.43.0-post.
|
|
142
|
+
"version": "1.43.0-post.20"
|
|
147
143
|
}
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
PER_LIBRARY_UNSUPPRESS_REL,
|
|
8
8
|
} from "../../../src/features/book/bookEditorSlice";
|
|
9
9
|
import { BookDetailsEditor } from "../../../src/components/BookDetailsEditor";
|
|
10
|
-
import { expect } from "chai";
|
|
11
10
|
import { store } from "../../../src/store";
|
|
12
11
|
import * as fetchMock from "fetch-mock-jest";
|
|
13
12
|
|
|
@@ -93,8 +92,8 @@ describe("BookDetails", () => {
|
|
|
93
92
|
const hideButton = queryByRole("button", { name: "Hide" });
|
|
94
93
|
const restoreButton = queryByRole("button", { name: "Restore" });
|
|
95
94
|
|
|
96
|
-
expect(hideButton).
|
|
97
|
-
expect(restoreButton).
|
|
95
|
+
expect(hideButton).toBeNull();
|
|
96
|
+
expect(restoreButton).toBeNull();
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
it("don't show restore button if not a library's admin", () => {
|
|
@@ -110,8 +109,8 @@ describe("BookDetails", () => {
|
|
|
110
109
|
const hideButton = queryByRole("button", { name: "Hide" });
|
|
111
110
|
const restoreButton = queryByRole("button", { name: "Restore" });
|
|
112
111
|
|
|
113
|
-
expect(hideButton).
|
|
114
|
-
expect(restoreButton).
|
|
112
|
+
expect(hideButton).toBeNull();
|
|
113
|
+
expect(restoreButton).toBeNull();
|
|
115
114
|
});
|
|
116
115
|
|
|
117
116
|
it("uses modal for suppress book confirmation", async () => {
|
|
@@ -149,8 +148,8 @@ describe("BookDetails", () => {
|
|
|
149
148
|
await user.click(cancelButton);
|
|
150
149
|
confirmButton = queryByRole("button", { name: "Suppress Availability" });
|
|
151
150
|
cancelButton = queryByRole("button", { name: "Cancel" });
|
|
152
|
-
expect(confirmButton).
|
|
153
|
-
expect(cancelButton).
|
|
151
|
+
expect(confirmButton).toBeNull();
|
|
152
|
+
expect(cancelButton).toBeNull();
|
|
154
153
|
|
|
155
154
|
// Clicking `Hide` again should show the modal again.
|
|
156
155
|
await user.click(hideButton);
|
|
@@ -163,15 +162,15 @@ describe("BookDetails", () => {
|
|
|
163
162
|
getByRole("button", { name: "Dismiss" });
|
|
164
163
|
|
|
165
164
|
// Check that the API was invoked.
|
|
166
|
-
expect(suppressBook.mock.calls.length).
|
|
167
|
-
expect(suppressBook.mock.calls[0][0]).
|
|
165
|
+
expect(suppressBook.mock.calls.length).toBe(1);
|
|
166
|
+
expect(suppressBook.mock.calls[0][0]).toBe("/suppress/href");
|
|
168
167
|
const fetchCalls = fetchMock.calls();
|
|
169
|
-
expect(fetchCalls.length).
|
|
168
|
+
expect(fetchCalls.length).toBe(1);
|
|
170
169
|
const fetchCall = fetchCalls[0];
|
|
171
170
|
const fetchOptions = fetchCalls[0][1];
|
|
172
|
-
expect(fetchCall[0]).
|
|
173
|
-
expect(fetchOptions["headers"]["X-CSRF-Token"]).
|
|
174
|
-
expect(fetchOptions["method"]).
|
|
171
|
+
expect(fetchCall[0]).toBe("/suppress/href");
|
|
172
|
+
expect(fetchOptions["headers"]["X-CSRF-Token"]).toContain("token");
|
|
173
|
+
expect(fetchOptions["method"]).toBe("POST");
|
|
175
174
|
});
|
|
176
175
|
it("uses modal for unsuppress book confirmation", async () => {
|
|
177
176
|
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
|
|
@@ -213,8 +212,8 @@ describe("BookDetails", () => {
|
|
|
213
212
|
await user.click(cancelButton);
|
|
214
213
|
confirmButton = queryByRole("button", { name: "Restore Availability" });
|
|
215
214
|
cancelButton = queryByRole("button", { name: "Cancel" });
|
|
216
|
-
expect(confirmButton).
|
|
217
|
-
expect(cancelButton).
|
|
215
|
+
expect(confirmButton).toBeNull();
|
|
216
|
+
expect(cancelButton).toBeNull();
|
|
218
217
|
|
|
219
218
|
// Clicking `Restore` again should show the modal again.
|
|
220
219
|
await user.click(restoreButton);
|
|
@@ -227,14 +226,14 @@ describe("BookDetails", () => {
|
|
|
227
226
|
getByRole("button", { name: "Dismiss" });
|
|
228
227
|
|
|
229
228
|
// Check that the API was invoked.
|
|
230
|
-
expect(unsuppressBook.mock.calls.length).
|
|
231
|
-
expect(unsuppressBook.mock.calls[0][0]).
|
|
229
|
+
expect(unsuppressBook.mock.calls.length).toBe(1);
|
|
230
|
+
expect(unsuppressBook.mock.calls[0][0]).toBe("/unsuppress/href");
|
|
232
231
|
const fetchCalls = fetchMock.calls();
|
|
233
|
-
expect(fetchCalls.length).
|
|
232
|
+
expect(fetchCalls.length).toBe(1);
|
|
234
233
|
const fetchCall = fetchCalls[0];
|
|
235
234
|
const fetchOptions = fetchCalls[0][1];
|
|
236
|
-
expect(fetchCall[0]).
|
|
237
|
-
expect(fetchOptions["headers"]["X-CSRF-Token"]).
|
|
238
|
-
expect(fetchOptions["method"]).
|
|
235
|
+
expect(fetchCall[0]).toBe("/unsuppress/href");
|
|
236
|
+
expect(fetchOptions["headers"]["X-CSRF-Token"]).toContain("token");
|
|
237
|
+
expect(fetchOptions["method"]).toBe("DELETE");
|
|
239
238
|
});
|
|
240
239
|
});
|
|
@@ -4,8 +4,8 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
|
|
5
5
|
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
6
|
import buildStore from "../../../src/store";
|
|
7
|
-
import genreData from "
|
|
8
|
-
import classificationsData from "
|
|
7
|
+
import genreData from "../fixtures/genreData";
|
|
8
|
+
import classificationsData from "../fixtures/classificationsData";
|
|
9
9
|
// Render the CONNECTED default export so that mapStateToProps / mapDispatchToProps
|
|
10
10
|
// are exercised. The genre tree and classifications are fetched on mount and fed
|
|
11
11
|
// back in through the Redux store; `book` and `refreshCatalog` are own props.
|
|
@@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
|
|
5
5
|
import ClassificationsForm from "../../../src/components/ClassificationsForm";
|
|
6
6
|
import { BookData } from "../../../src/interfaces";
|
|
7
|
-
import genreData from "
|
|
7
|
+
import genreData from "../fixtures/genreData";
|
|
8
8
|
|
|
9
9
|
// ClassificationsForm is a plain (unconnected) class component, so a bare
|
|
10
10
|
// `render` is enough — no store/context/router needed.
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
3
|
|
|
4
4
|
import ClassificationsTable from "../../../src/components/ClassificationsTable";
|
|
5
|
-
import classificationsData from "
|
|
5
|
+
import classificationsData from "../fixtures/classificationsData";
|
|
6
6
|
|
|
7
7
|
// Mirrors ClassificationsTable's own `readableType`: it strips the genres URL
|
|
8
8
|
// prefix so the displayed type is human-readable.
|
|
@@ -313,7 +313,7 @@ describe("CustomListsForBook", () => {
|
|
|
313
313
|
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
it("
|
|
316
|
+
it("shows the new book's lists when it is rerendered for another book", () => {
|
|
317
317
|
const commonProps = {
|
|
318
318
|
csrfToken: "token",
|
|
319
319
|
library: "library",
|
|
@@ -331,10 +331,12 @@ describe("CustomListsForBook", () => {
|
|
|
331
331
|
customListsForBook={listsForBook as any}
|
|
332
332
|
/>
|
|
333
333
|
);
|
|
334
|
-
// list 2 (the book's only list) is in the current column
|
|
335
|
-
|
|
334
|
+
// list 2 (the book's only list) is in the current column, which renders
|
|
335
|
+
// each list as a link; the add-menu renders options instead.
|
|
336
|
+
expect(screen.getByRole("link", { name: "list 2" })).toBeInTheDocument();
|
|
336
337
|
|
|
337
|
-
//
|
|
338
|
+
// The current column renders straight from customListsForBook, so a new
|
|
339
|
+
// book's lists replace the old ones on rerender.
|
|
338
340
|
rerender(
|
|
339
341
|
<UnconnectedCustomListsForBook
|
|
340
342
|
{...commonProps}
|
|
@@ -343,6 +345,9 @@ describe("CustomListsForBook", () => {
|
|
|
343
345
|
customListsForBook={[allCustomLists[0]] as any}
|
|
344
346
|
/>
|
|
345
347
|
);
|
|
346
|
-
expect(screen.
|
|
348
|
+
expect(screen.getByRole("link", { name: "list 1" })).toBeInTheDocument();
|
|
349
|
+
expect(
|
|
350
|
+
screen.queryByRole("link", { name: "list 2" })
|
|
351
|
+
).not.toBeInTheDocument();
|
|
347
352
|
});
|
|
348
353
|
});
|
|
@@ -3,7 +3,7 @@ import { render, screen, fireEvent } from "@testing-library/react";
|
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
|
|
5
5
|
import GenreForm from "../../../src/components/GenreForm";
|
|
6
|
-
import genreData from "
|
|
6
|
+
import genreData from "../fixtures/genreData";
|
|
7
7
|
|
|
8
8
|
const genreOptions = Object.keys(genreData["Fiction"]).map(
|
|
9
9
|
(name) => genreData["Fiction"][name]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
|
-
import { stub } from "sinon";
|
|
5
4
|
import { LaneData } from "../../../src/interfaces";
|
|
6
5
|
import Lane from "../../../src/components/Lane";
|
|
7
6
|
|
|
@@ -20,8 +19,8 @@ jest.mock("react-router", () => ({
|
|
|
20
19
|
),
|
|
21
20
|
}));
|
|
22
21
|
|
|
23
|
-
const renderLanes =
|
|
24
|
-
const toggleLaneVisibility =
|
|
22
|
+
const renderLanes = jest.fn();
|
|
23
|
+
const toggleLaneVisibility = jest.fn();
|
|
25
24
|
|
|
26
25
|
function createLaneData(displayName: string, isAutomated: boolean): LaneData {
|
|
27
26
|
return {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
|
-
import { stub } from "sinon";
|
|
5
4
|
import { LaneData } from "../../../src/interfaces";
|
|
6
5
|
import LaneEditor from "../../../src/components/LaneEditor";
|
|
7
6
|
import * as navigate from "../../../src/utils/navigate";
|
|
@@ -62,11 +61,15 @@ const customListsData = [
|
|
|
62
61
|
{ id: 1, name: "list 1", entries: [], is_owner: true, is_shared: false },
|
|
63
62
|
];
|
|
64
63
|
|
|
65
|
-
const editLane =
|
|
64
|
+
const editLane = jest
|
|
65
|
+
.fn()
|
|
66
|
+
.mockReturnValue(new Promise<void>((resolve) => resolve()));
|
|
66
67
|
|
|
67
|
-
const deleteLane =
|
|
68
|
+
const deleteLane = jest
|
|
69
|
+
.fn()
|
|
70
|
+
.mockReturnValue(new Promise<void>((resolve) => resolve()));
|
|
68
71
|
|
|
69
|
-
const toggleLaneVisibility =
|
|
72
|
+
const toggleLaneVisibility = jest.fn();
|
|
70
73
|
|
|
71
74
|
function createLaneData(displayName: string, isAutomated: boolean): LaneData {
|
|
72
75
|
return {
|
|
@@ -104,7 +107,7 @@ describe("LaneEditor", () => {
|
|
|
104
107
|
editOrCreate="edit"
|
|
105
108
|
editLane={editLane}
|
|
106
109
|
deleteLane={deleteLane}
|
|
107
|
-
findParentOfLane={
|
|
110
|
+
findParentOfLane={jest.fn().mockReturnValue(laneData)}
|
|
108
111
|
toggleLaneVisibility={toggleLaneVisibility}
|
|
109
112
|
/>
|
|
110
113
|
);
|
|
@@ -139,7 +142,7 @@ describe("LaneEditor", () => {
|
|
|
139
142
|
editOrCreate="edit"
|
|
140
143
|
editLane={editLane}
|
|
141
144
|
deleteLane={deleteLane}
|
|
142
|
-
findParentOfLane={
|
|
145
|
+
findParentOfLane={jest.fn().mockReturnValue(laneData)}
|
|
143
146
|
toggleLaneVisibility={toggleLaneVisibility}
|
|
144
147
|
/>
|
|
145
148
|
);
|
|
@@ -177,7 +180,7 @@ describe("LaneEditor", () => {
|
|
|
177
180
|
editOrCreate="edit"
|
|
178
181
|
editLane={editLane}
|
|
179
182
|
deleteLane={deleteLane}
|
|
180
|
-
findParentOfLane={
|
|
183
|
+
findParentOfLane={jest.fn().mockReturnValue(laneData)}
|
|
181
184
|
toggleLaneVisibility={toggleLaneVisibility}
|
|
182
185
|
/>
|
|
183
186
|
);
|
|
@@ -28,13 +28,6 @@ const normalizedData = normalizeStatistics(statisticsApiResponseData);
|
|
|
28
28
|
global.ResizeObserver = require("resize-observer-polyfill");
|
|
29
29
|
|
|
30
30
|
describe("Dashboard Statistics", () => {
|
|
31
|
-
// NB: This adds test to the already existing tests in:
|
|
32
|
-
// - `src/components/__tests__/LibraryStats-test.tsx`.
|
|
33
|
-
// - `src/components/__tests__/SingleStatListItem-test.tsx`.
|
|
34
|
-
//
|
|
35
|
-
// Those tests should eventually be migrated here and
|
|
36
|
-
// adapted to the Jest/React Testing Library paradigm.
|
|
37
|
-
|
|
38
31
|
// Configure standard constructors so that RTK Query works in tests with FetchMockJest
|
|
39
32
|
Object.assign(fetchMock.config, {
|
|
40
33
|
fetch,
|
|
@@ -3,7 +3,6 @@ import reducer, {
|
|
|
3
3
|
getBookData,
|
|
4
4
|
submitBookData,
|
|
5
5
|
} from "../../../src/features/book/bookEditorSlice";
|
|
6
|
-
import { expect } from "chai";
|
|
7
6
|
import * as fetchMock from "fetch-mock-jest";
|
|
8
7
|
import { BookData } from "@thepalaceproject/web-opds-client/lib/interfaces";
|
|
9
8
|
import { RequestError } from "@thepalaceproject/web-opds-client/lib/DataFetcher";
|
|
@@ -57,12 +56,12 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
57
56
|
|
|
58
57
|
describe("reducers...", () => {
|
|
59
58
|
it("should return the initial state from undefined, if no action is passed", () => {
|
|
60
|
-
expect(reducer(undefined, { type: "unknown" })).
|
|
59
|
+
expect(reducer(undefined, { type: "unknown" })).toStrictEqual(
|
|
61
60
|
initialState
|
|
62
61
|
);
|
|
63
62
|
});
|
|
64
63
|
it("should return the initial state from initialState, if no action is passed", () => {
|
|
65
|
-
expect(reducer(initialState, { type: "unknown" })).
|
|
64
|
+
expect(reducer(initialState, { type: "unknown" })).toStrictEqual(
|
|
66
65
|
initialState
|
|
67
66
|
);
|
|
68
67
|
});
|
|
@@ -70,7 +69,7 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
70
69
|
// This is dispatched by `web-opds`client`, but we need to handle it, too.
|
|
71
70
|
const action = { type: ActionCreator.BOOK_CLEAR };
|
|
72
71
|
|
|
73
|
-
expect(reducer(fetchedState, action)).
|
|
72
|
+
expect(reducer(fetchedState, action)).toStrictEqual(initialState);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
it("should handle getBookData.pending", () => {
|
|
@@ -81,11 +80,11 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
81
80
|
const previousState = { ...initialState, url: null, isFetching: false };
|
|
82
81
|
const state = reducer(previousState, action);
|
|
83
82
|
|
|
84
|
-
expect(state.url).
|
|
85
|
-
expect(state.data).
|
|
86
|
-
expect(state.isFetching).
|
|
87
|
-
expect(state.fetchError).
|
|
88
|
-
expect(state.editError).
|
|
83
|
+
expect(state.url).toBe("https://example.com/book");
|
|
84
|
+
expect(state.data).toBeNull();
|
|
85
|
+
expect(state.isFetching).toBe(true);
|
|
86
|
+
expect(state.fetchError).toBeNull();
|
|
87
|
+
expect(state.editError).toBeNull();
|
|
89
88
|
});
|
|
90
89
|
it("should handle getBookData.fulfilled", () => {
|
|
91
90
|
const action = {
|
|
@@ -101,11 +100,11 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
101
100
|
};
|
|
102
101
|
const state = reducer(previousState, action);
|
|
103
102
|
|
|
104
|
-
expect(state.url).
|
|
105
|
-
expect(state.data).
|
|
106
|
-
expect(state.isFetching).
|
|
107
|
-
expect(state.fetchError).
|
|
108
|
-
expect(state.editError).
|
|
103
|
+
expect(state.url).toBe("https://example.com/book");
|
|
104
|
+
expect(state.data).toStrictEqual(bookData);
|
|
105
|
+
expect(state.isFetching).toBe(false);
|
|
106
|
+
expect(state.fetchError).toBeNull();
|
|
107
|
+
expect(state.editError).toBeNull();
|
|
109
108
|
});
|
|
110
109
|
it("should handle getBookData.rejected", () => {
|
|
111
110
|
const errorObject = { error: "some error object" };
|
|
@@ -122,11 +121,11 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
122
121
|
};
|
|
123
122
|
const state = reducer(previousState, action);
|
|
124
123
|
|
|
125
|
-
expect(state.url).
|
|
126
|
-
expect(state.data).
|
|
127
|
-
expect(state.isFetching).
|
|
128
|
-
expect(state.fetchError).
|
|
129
|
-
expect(state.editError).
|
|
124
|
+
expect(state.url).toBe("https://example.com/book");
|
|
125
|
+
expect(state.data).toBeNull();
|
|
126
|
+
expect(state.isFetching).toBe(false);
|
|
127
|
+
expect(state.fetchError).toStrictEqual(errorObject);
|
|
128
|
+
expect(state.editError).toBeNull();
|
|
130
129
|
});
|
|
131
130
|
|
|
132
131
|
it("should handle submitBookData.pending", () => {
|
|
@@ -134,7 +133,7 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
134
133
|
const previousState = { ...fetchedState, isFetching: false };
|
|
135
134
|
const state = reducer(previousState, action);
|
|
136
135
|
|
|
137
|
-
expect(state).
|
|
136
|
+
expect(state).toStrictEqual({ ...fetchedState, isFetching: true });
|
|
138
137
|
});
|
|
139
138
|
it("should handle submitBookData.fulfilled", () => {
|
|
140
139
|
const action = {
|
|
@@ -144,7 +143,7 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
144
143
|
const previousState = { ...fetchedState, isFetching: true };
|
|
145
144
|
const state = reducer(previousState, action);
|
|
146
145
|
|
|
147
|
-
expect(state).
|
|
146
|
+
expect(state).toStrictEqual({
|
|
148
147
|
...fetchedState,
|
|
149
148
|
isFetching: false,
|
|
150
149
|
editError: null,
|
|
@@ -158,7 +157,7 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
158
157
|
const previousState = { ...fetchedState, isFetching: true };
|
|
159
158
|
const state = reducer(previousState, action);
|
|
160
159
|
|
|
161
|
-
expect(state).
|
|
160
|
+
expect(state).toStrictEqual({
|
|
162
161
|
...fetchedState,
|
|
163
162
|
isFetching: false,
|
|
164
163
|
editError: "some value",
|
|
@@ -200,22 +199,22 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
200
199
|
const dispatchCalls = dispatch.mock.calls;
|
|
201
200
|
|
|
202
201
|
const payload = result.payload as BookData;
|
|
203
|
-
expect(payload.id).
|
|
202
|
+
expect(payload.id).toBe(
|
|
204
203
|
"urn:uuid:1cca9468-c447-4303-bc5a-c57470b85cb1"
|
|
205
204
|
);
|
|
206
|
-
expect(payload.title).
|
|
205
|
+
expect(payload.title).toBe(
|
|
207
206
|
"Tea-Cup Reading and Fortune-Telling by Tea Leaves"
|
|
208
207
|
);
|
|
209
208
|
|
|
210
|
-
expect(dispatchCalls.length).
|
|
211
|
-
expect(dispatchCalls[0][0].type).
|
|
212
|
-
expect(dispatchCalls[0][0].payload).
|
|
213
|
-
expect(dispatchCalls[0][0].meta.arg).
|
|
209
|
+
expect(dispatchCalls.length).toBe(2);
|
|
210
|
+
expect(dispatchCalls[0][0].type).toBe(getBookData.pending.type);
|
|
211
|
+
expect(dispatchCalls[0][0].payload).toBe(undefined);
|
|
212
|
+
expect(dispatchCalls[0][0].meta.arg).toStrictEqual({
|
|
214
213
|
url: goodBookUrl,
|
|
215
214
|
});
|
|
216
|
-
expect(dispatchCalls[1][0].type).
|
|
217
|
-
expect(dispatchCalls[1][0].payload).
|
|
218
|
-
expect(dispatchCalls[1][0].meta.arg).
|
|
215
|
+
expect(dispatchCalls[1][0].type).toBe(getBookData.fulfilled.type);
|
|
216
|
+
expect(dispatchCalls[1][0].payload).toStrictEqual(payload);
|
|
217
|
+
expect(dispatchCalls[1][0].meta.arg).toStrictEqual({
|
|
219
218
|
url: goodBookUrl,
|
|
220
219
|
});
|
|
221
220
|
});
|
|
@@ -226,18 +225,18 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
226
225
|
const dispatchCalls = dispatch.mock.calls;
|
|
227
226
|
|
|
228
227
|
const payload = result.payload as RequestError;
|
|
229
|
-
expect(payload.response).
|
|
230
|
-
expect(payload.url).
|
|
228
|
+
expect(payload.response).toBe(FETCH_OPDS_PARSE_ERROR_MESSAGE);
|
|
229
|
+
expect(payload.url).toBe(brokenBookUrl);
|
|
231
230
|
|
|
232
|
-
expect(dispatchCalls.length).
|
|
233
|
-
expect(dispatchCalls[0][0].type).
|
|
234
|
-
expect(dispatchCalls[0][0].payload).
|
|
235
|
-
expect(dispatchCalls[0][0].meta.arg).
|
|
231
|
+
expect(dispatchCalls.length).toBe(2);
|
|
232
|
+
expect(dispatchCalls[0][0].type).toBe(getBookData.pending.type);
|
|
233
|
+
expect(dispatchCalls[0][0].payload).toBe(undefined);
|
|
234
|
+
expect(dispatchCalls[0][0].meta.arg).toStrictEqual({
|
|
236
235
|
url: brokenBookUrl,
|
|
237
236
|
});
|
|
238
|
-
expect(dispatchCalls[1][0].type).
|
|
239
|
-
expect(dispatchCalls[1][0].payload).
|
|
240
|
-
expect(dispatchCalls[1][0].meta.arg).
|
|
237
|
+
expect(dispatchCalls[1][0].type).toBe(getBookData.rejected.type);
|
|
238
|
+
expect(dispatchCalls[1][0].payload).toStrictEqual(payload);
|
|
239
|
+
expect(dispatchCalls[1][0].meta.arg).toStrictEqual({
|
|
241
240
|
url: brokenBookUrl,
|
|
242
241
|
});
|
|
243
242
|
});
|
|
@@ -249,20 +248,20 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
249
248
|
|
|
250
249
|
const payload = result.payload as RequestError;
|
|
251
250
|
|
|
252
|
-
expect(result.type).
|
|
253
|
-
expect(result.meta.arg).
|
|
254
|
-
expect(payload.response).
|
|
255
|
-
expect(payload.url).
|
|
251
|
+
expect(result.type).toBe(getBookData.rejected.type);
|
|
252
|
+
expect(result.meta.arg).toStrictEqual({ url: errorBookUrl });
|
|
253
|
+
expect(payload.response).toBe("Internal server error");
|
|
254
|
+
expect(payload.url).toBe(errorBookUrl);
|
|
256
255
|
|
|
257
|
-
expect(dispatchCalls.length).
|
|
258
|
-
expect(dispatchCalls[0][0].type).
|
|
259
|
-
expect(dispatchCalls[0][0].payload).
|
|
260
|
-
expect(dispatchCalls[0][0].meta.arg).
|
|
256
|
+
expect(dispatchCalls.length).toBe(2);
|
|
257
|
+
expect(dispatchCalls[0][0].type).toBe(getBookData.pending.type);
|
|
258
|
+
expect(dispatchCalls[0][0].payload).toBe(undefined);
|
|
259
|
+
expect(dispatchCalls[0][0].meta.arg).toStrictEqual({
|
|
261
260
|
url: errorBookUrl,
|
|
262
261
|
});
|
|
263
|
-
expect(dispatchCalls[1][0].type).
|
|
264
|
-
expect(dispatchCalls[1][0].payload).
|
|
265
|
-
expect(dispatchCalls[1][0].meta.arg).
|
|
262
|
+
expect(dispatchCalls[1][0].type).toBe(getBookData.rejected.type);
|
|
263
|
+
expect(dispatchCalls[1][0].payload).toStrictEqual(payload);
|
|
264
|
+
expect(dispatchCalls[1][0].meta.arg).toStrictEqual({
|
|
266
265
|
url: errorBookUrl,
|
|
267
266
|
});
|
|
268
267
|
});
|
|
@@ -327,27 +326,23 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
327
326
|
const dispatchCalls = dispatch.mock.calls;
|
|
328
327
|
const fetchCalls = fetchMock.calls();
|
|
329
328
|
|
|
330
|
-
expect(fetchCalls.length).
|
|
331
|
-
expect(fetchCalls[0].identifier).
|
|
332
|
-
expect(
|
|
333
|
-
|
|
334
|
-
)
|
|
329
|
+
expect(fetchCalls.length).toBe(1);
|
|
330
|
+
expect(fetchCalls[0].identifier).toBe("valid-csrf-token-post");
|
|
331
|
+
expect((fetchCalls[0][1].headers as Headers).get(csrfTokenHeader)).toBe(
|
|
332
|
+
validCsrfToken
|
|
333
|
+
);
|
|
335
334
|
|
|
336
|
-
expect(fetchCalls[0][0]).
|
|
337
|
-
expect(fetchCalls[0][1].method).
|
|
338
|
-
expect(fetchCalls[0][1].body).
|
|
335
|
+
expect(fetchCalls[0][0]).toBe(editBookUrl);
|
|
336
|
+
expect(fetchCalls[0][1].method).toBe("POST");
|
|
337
|
+
expect(fetchCalls[0][1].body).toBe(formData);
|
|
339
338
|
|
|
340
|
-
expect(dispatchCalls.length).
|
|
341
|
-
expect(dispatchCalls[0][0].type).
|
|
342
|
-
expect(dispatchCalls[0][0].payload).
|
|
339
|
+
expect(dispatchCalls.length).toBe(3);
|
|
340
|
+
expect(dispatchCalls[0][0].type).toBe(submitBookData.pending.type);
|
|
341
|
+
expect(dispatchCalls[0][0].payload).toBe(undefined);
|
|
343
342
|
// On a successful update, the second dispatch is to re-fetch the updated book data.
|
|
344
343
|
// The third dispatch is for the fulfilled action.
|
|
345
|
-
expect(dispatchCalls[2][0].type).
|
|
346
|
-
|
|
347
|
-
);
|
|
348
|
-
expect(dispatchCalls[2][0].payload.body.toString()).to.equal(
|
|
349
|
-
"Success!"
|
|
350
|
-
);
|
|
344
|
+
expect(dispatchCalls[2][0].type).toBe(submitBookData.fulfilled.type);
|
|
345
|
+
expect(dispatchCalls[2][0].payload.body.toString()).toBe("Success!");
|
|
351
346
|
});
|
|
352
347
|
it("should fail, if the user is unauthorized", async () => {
|
|
353
348
|
const csrfToken = "invalid-token";
|
|
@@ -365,24 +360,24 @@ describe("Redux bookEditorSlice...", () => {
|
|
|
365
360
|
const dispatchCalls = dispatch.mock.calls;
|
|
366
361
|
const fetchCalls = fetchMock.calls();
|
|
367
362
|
|
|
368
|
-
expect(fetchCalls.length).
|
|
369
|
-
expect(fetchCalls[0].identifier).
|
|
363
|
+
expect(fetchCalls.length).toBe(1);
|
|
364
|
+
expect(fetchCalls[0].identifier).toBe("invalid-csrf-token-post");
|
|
370
365
|
expect(
|
|
371
366
|
(fetchCalls[0][1].headers as Headers).get(csrfTokenHeader)
|
|
372
|
-
).not.
|
|
367
|
+
).not.toBe(validCsrfToken);
|
|
373
368
|
|
|
374
|
-
expect(fetchCalls[0][0]).
|
|
375
|
-
expect(fetchCalls[0][1].method).
|
|
376
|
-
expect(fetchCalls[0][1].body).
|
|
369
|
+
expect(fetchCalls[0][0]).toBe(editBookUrl);
|
|
370
|
+
expect(fetchCalls[0][1].method).toBe("POST");
|
|
371
|
+
expect(fetchCalls[0][1].body).toBe(formData);
|
|
377
372
|
|
|
378
|
-
expect(dispatchCalls.length).
|
|
379
|
-
expect(dispatchCalls[0][0].type).
|
|
380
|
-
expect(dispatchCalls[0][0].payload).
|
|
373
|
+
expect(dispatchCalls.length).toBe(2);
|
|
374
|
+
expect(dispatchCalls[0][0].type).toBe(submitBookData.pending.type);
|
|
375
|
+
expect(dispatchCalls[0][0].payload).toBe(undefined);
|
|
381
376
|
// There is no re-fetch on a failed request, ...
|
|
382
377
|
// ...so the second dispatch is for the rejected action.
|
|
383
|
-
expect(dispatchCalls[1][0].type).
|
|
384
|
-
expect(dispatchCalls[1][0].payload.status).
|
|
385
|
-
expect(dispatchCalls[1][0].payload.response).
|
|
378
|
+
expect(dispatchCalls[1][0].type).toBe(submitBookData.rejected.type);
|
|
379
|
+
expect(dispatchCalls[1][0].payload.status).toBe(400);
|
|
380
|
+
expect(dispatchCalls[1][0].payload.response).toBe(
|
|
386
381
|
"There was an error saving your changes."
|
|
387
382
|
);
|
|
388
383
|
});
|