@thepalaceproject/circulation-admin 0.4.2 → 1.5.1

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.
Files changed (38) hide show
  1. package/.nvmrc +1 -0
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +1 -1
  4. package/dist/0db1520f47986b6c755a.svg +1 -0
  5. package/dist/45eb1d236a736caa24dd.woff2 +1 -0
  6. package/dist/4c6f1cd9993ba8a53b8e.ttf +1 -0
  7. package/dist/5e9505a87e4d8ecb2017.eot +1 -0
  8. package/dist/6563aa3790be8329e4f2.svg +1 -0
  9. package/dist/7a065a1c0cb2d586cecb.woff +1 -0
  10. package/dist/7d6ec71e2466a9fd777f.woff2 +1 -0
  11. package/dist/circulation-admin.css +4 -4
  12. package/dist/circulation-admin.js +2 -397
  13. package/dist/circulation-admin.js.LICENSE.txt +125 -0
  14. package/dist/dec4ea00820558e24672.ttf +1 -0
  15. package/dist/e5c0c62d732823225aaa.eot +1 -0
  16. package/dist/f34ea237f268661e9d00.woff +1 -0
  17. package/jest.config.js +6 -0
  18. package/nightwatch.json +3 -3
  19. package/package.json +47 -30
  20. package/tests/{README.md → browser/README.md} +3 -0
  21. package/tests/jest/README.md +6 -0
  22. package/tests/jest/components/AdvancedSearchBuilder.test.tsx +39 -0
  23. package/tests/jest/components/EditableInput.test.tsx +64 -0
  24. package/tests/jest/components/IndividualAdminEditForm.test.tsx +126 -0
  25. package/tests/jest/components/Lane.test.tsx +78 -0
  26. package/tests/jest/components/LaneEditor.test.tsx +148 -0
  27. package/tests/jest/jest-setup.ts +1 -0
  28. package/tests/jest/sample/sample.test.js +3 -0
  29. package/tests/jest/testUtils/renderWithContext.tsx +36 -0
  30. package/tsconfig.json +8 -4
  31. package/webpack.common.js +9 -5
  32. package/webpack.dev-server.config.js +1 -1
  33. package/webpack.dev.config.js +1 -1
  34. package/webpack.prod.config.js +1 -1
  35. /package/dist/{89889688147bd7575d6327160d64e760.svg → 060b2710bdbbe3dfe48b58d59bd5f1fb.svg} +0 -0
  36. /package/dist/{b06871f281fee6b241d60582ae9369b9.ttf → 1e59d2330b4c6deb84b340635ed36249.ttf} +0 -0
  37. /package/dist/{674f50d287a8c48dc19ba404d20fe713.eot → 8b43027f47b20503057dfbbaa9401fef.eot} +0 -0
  38. /package/dist/{912ec66d7572ff821749319396470bde.svg → c1e38fd9e0e74ba58f7a2b77ef29fdd3.svg} +0 -0
@@ -0,0 +1,64 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import EditableInput from "../../../src/components/EditableInput";
4
+
5
+ describe("EditableInput", () => {
6
+ it("renders an accessible description if a description prop is supplied and a label prop is supplied", () => {
7
+ const label = "input1";
8
+ const description = "this is a field";
9
+
10
+ render(
11
+ <EditableInput
12
+ label={label}
13
+ optionalText={false}
14
+ description={description}
15
+ />
16
+ );
17
+
18
+ const textbox = screen.getByRole("textbox", { name: label });
19
+
20
+ expect(textbox).toHaveAccessibleDescription(description);
21
+ });
22
+
23
+ it("renders an accessible description if a description prop is supplied and a label prop is not supplied", () => {
24
+ const description = "this is a field";
25
+
26
+ render(<EditableInput optionalText={false} description={description} />);
27
+
28
+ const textbox = screen.getByRole("textbox");
29
+
30
+ expect(textbox).toHaveAccessibleDescription(description);
31
+ });
32
+
33
+ it("renders an accessible description if optionalText is true", () => {
34
+ render(<EditableInput optionalText={true} />);
35
+
36
+ const textbox = screen.getByRole("textbox");
37
+
38
+ expect(textbox).toHaveAccessibleDescription(/optional/i);
39
+ });
40
+
41
+ it("associates accessible descriptions with the correct inputs when multiple instances are present", () => {
42
+ const descriptions = ["desc 1", "desc 2", "desc 3"];
43
+
44
+ render(
45
+ <div>
46
+ <EditableInput optionalText={false} description={descriptions[0]} />
47
+ <EditableInput optionalText={false} />
48
+ <EditableInput optionalText={false} />
49
+ <EditableInput optionalText={false} description={descriptions[1]} />
50
+ <EditableInput optionalText={false} description={descriptions[2]} />
51
+ <EditableInput optionalText={false} />
52
+ </div>
53
+ );
54
+
55
+ const textboxes = screen.getAllByRole("textbox");
56
+
57
+ expect(textboxes[0]).toHaveAccessibleDescription(descriptions[0]);
58
+ expect(textboxes[1]).toHaveAccessibleDescription("");
59
+ expect(textboxes[2]).toHaveAccessibleDescription("");
60
+ expect(textboxes[3]).toHaveAccessibleDescription(descriptions[1]);
61
+ expect(textboxes[4]).toHaveAccessibleDescription(descriptions[2]);
62
+ expect(textboxes[5]).toHaveAccessibleDescription("");
63
+ });
64
+ });
@@ -0,0 +1,126 @@
1
+ import * as React from "react";
2
+ import { screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import IndividualAdminEditForm from "../../../src/components/IndividualAdminEditForm";
5
+ import renderWithContext from "../testUtils/renderWithContext";
6
+
7
+ describe("IndividualAdminEditForm", () => {
8
+ it("clears the role checkboxes after save", async () => {
9
+ const user = userEvent.setup();
10
+
11
+ const contextProviderProps = {
12
+ csrfToken: "",
13
+ roles: [
14
+ {
15
+ role: "system",
16
+ },
17
+ ],
18
+ };
19
+
20
+ const props = {
21
+ data: {
22
+ allLibraries: [
23
+ {
24
+ name: "Alpha",
25
+ short_name: "alpha",
26
+ uuid: "a3247ce9-9639-426b-bb09-82e9cb4cf44b",
27
+ },
28
+ {
29
+ name: "Beta",
30
+ short_name: "beta",
31
+ uuid: "da80cd40-7a87-41db-a789-5f1e87732aeb",
32
+ },
33
+ {
34
+ name: "Gamma",
35
+ short_name: "gamma",
36
+ uuid: "15f32675-73f5-46f3-91f4-837b933bc7b1",
37
+ },
38
+ ],
39
+ },
40
+ disabled: false,
41
+ listDataKey: "",
42
+ urlBase: "",
43
+ };
44
+
45
+ const { rerender } = renderWithContext(
46
+ <IndividualAdminEditForm {...props} />,
47
+ contextProviderProps
48
+ );
49
+
50
+ const systemAdminCheckbox = screen.getByRole("checkbox", {
51
+ name: /^system admin$/i,
52
+ });
53
+
54
+ const allAdminCheckbox = screen.getByRole("checkbox", {
55
+ name: /^administrator$/i,
56
+ });
57
+
58
+ const allUserCheckbox = screen.getByRole("checkbox", {
59
+ name: /^user$/i,
60
+ });
61
+
62
+ const alphaAdminCheckbox = screen.getByRole("checkbox", {
63
+ name: /administrator of alpha/i,
64
+ });
65
+
66
+ const alphaUserCheckbox = screen.getByRole("checkbox", {
67
+ name: /user of alpha/i,
68
+ });
69
+
70
+ const betaAdminCheckbox = screen.getByRole("checkbox", {
71
+ name: /administrator of beta/i,
72
+ });
73
+
74
+ const betaUserCheckbox = screen.getByRole("checkbox", {
75
+ name: /user of beta/i,
76
+ });
77
+
78
+ const gammaAdminCheckbox = screen.getByRole("checkbox", {
79
+ name: /administrator of gamma/i,
80
+ });
81
+
82
+ const gammaUserCheckbox = screen.getByRole("checkbox", {
83
+ name: /user of gamma/i,
84
+ });
85
+
86
+ expect(systemAdminCheckbox).not.toBeChecked();
87
+
88
+ await user.click(systemAdminCheckbox);
89
+
90
+ expect(systemAdminCheckbox).toBeChecked();
91
+
92
+ expect(allAdminCheckbox).toBeChecked();
93
+ expect(allUserCheckbox).toBeChecked();
94
+
95
+ expect(alphaAdminCheckbox).toBeChecked();
96
+ expect(alphaUserCheckbox).toBeChecked();
97
+
98
+ expect(betaAdminCheckbox).toBeChecked();
99
+ expect(betaUserCheckbox).toBeChecked();
100
+
101
+ expect(gammaAdminCheckbox).toBeChecked();
102
+ expect(gammaUserCheckbox).toBeChecked();
103
+
104
+ const nextProps = {
105
+ ...props,
106
+ // Existence of the responseBody prop indicates that the form was just saved.
107
+ responseBody: "some response",
108
+ };
109
+
110
+ rerender(<IndividualAdminEditForm {...nextProps} />);
111
+
112
+ expect(systemAdminCheckbox).not.toBeChecked();
113
+
114
+ expect(allAdminCheckbox).not.toBeChecked();
115
+ expect(allUserCheckbox).not.toBeChecked();
116
+
117
+ expect(alphaAdminCheckbox).not.toBeChecked();
118
+ expect(alphaUserCheckbox).not.toBeChecked();
119
+
120
+ expect(betaAdminCheckbox).not.toBeChecked();
121
+ expect(betaUserCheckbox).not.toBeChecked();
122
+
123
+ expect(gammaAdminCheckbox).not.toBeChecked();
124
+ expect(gammaUserCheckbox).not.toBeChecked();
125
+ });
126
+ });
@@ -0,0 +1,78 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { stub } from "sinon";
4
+ import { LaneData } from "../../../src/interfaces";
5
+ import Lane from "../../../src/components/Lane";
6
+
7
+ // Mock the Link component from React Router, so we can verify that it gets rendered with the
8
+ // expected props. This serves as an example of how to do something analogous to Enzyme's shallow
9
+ // rendering, when we don't want/need to render the whole component tree down to HTML elements to
10
+ // test something. This technique is useful for testing components in isolation (unit testing),
11
+ // instead of the integration testing that RTL focuses on.
12
+
13
+ jest.mock("react-router", () => ({
14
+ ...jest.requireActual("react-router"),
15
+ Link: (props) => (
16
+ <div data-testid="Link" data-to={props.to}>
17
+ {props.children}
18
+ </div>
19
+ ),
20
+ }));
21
+
22
+ const renderLanes = stub();
23
+ const toggleLaneVisibility = stub();
24
+
25
+ function createLaneData(displayName: string, isAutomated: boolean): LaneData {
26
+ return {
27
+ id: 1,
28
+ display_name: displayName,
29
+ visible: true,
30
+ count: 5,
31
+ sublanes: [],
32
+ // The absence/presence of custom list ids determines if a lane is automated or custom.
33
+ custom_list_ids: isAutomated ? [] : [1],
34
+ inherit_parent_restrictions: true,
35
+ };
36
+ }
37
+
38
+ describe("Lane", () => {
39
+ it("renders an edit link on a custom lane", () => {
40
+ const laneData = createLaneData("Custom Lane", false);
41
+
42
+ render(
43
+ <Lane
44
+ lane={laneData}
45
+ active={false}
46
+ library="test_library"
47
+ orderChanged={false}
48
+ renderLanes={renderLanes}
49
+ toggleLaneVisibility={toggleLaneVisibility}
50
+ />
51
+ );
52
+
53
+ const editLink = screen.getAllByTestId("Link")[0];
54
+
55
+ expect(editLink).toHaveAttribute("data-to", expect.stringMatching(/edit/i));
56
+ expect(editLink).toHaveTextContent(/edit/i);
57
+ });
58
+
59
+ it("renders an edit link on an automated lane", async () => {
60
+ const laneData = createLaneData("Automated Lane", true);
61
+
62
+ render(
63
+ <Lane
64
+ lane={laneData}
65
+ active={false}
66
+ library="test_library"
67
+ orderChanged={false}
68
+ renderLanes={renderLanes}
69
+ toggleLaneVisibility={toggleLaneVisibility}
70
+ />
71
+ );
72
+
73
+ const editLink = screen.getAllByTestId("Link")[0];
74
+
75
+ expect(editLink).toHaveAttribute("data-to", expect.stringMatching(/edit/i));
76
+ expect(editLink).toHaveTextContent(/edit/i);
77
+ });
78
+ });
@@ -0,0 +1,148 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { stub } from "sinon";
4
+ import { LaneData } from "../../../src/interfaces";
5
+ import LaneEditor from "../../../src/components/LaneEditor";
6
+
7
+ // Mock the LaneCustomListsEditor so we can verify that it is or isn't rendered. This serves as an
8
+ // example of how to do something analogous to Enzyme's shallow rendering, when we don't want/need
9
+ // to render the whole component tree down to HTML elements to test something. This technique is
10
+ // useful for testing components in isolation (unit testing), instead of the integration testing
11
+ // that RTL focuses on.
12
+
13
+ jest.mock("../../../src/components/LaneCustomListsEditor", () => ({
14
+ __esModule: true,
15
+ default: (props) => <div data-testid="LaneCustomListsEditor" />,
16
+ }));
17
+
18
+ const customListsData = [
19
+ { id: 1, name: "list 1", entries: [], is_owner: true, is_shared: false },
20
+ ];
21
+
22
+ const editLane = stub().returns(
23
+ new Promise<void>((resolve) => resolve())
24
+ );
25
+
26
+ const deleteLane = stub().returns(
27
+ new Promise<void>((resolve) => resolve())
28
+ );
29
+
30
+ const toggleLaneVisibility = stub();
31
+
32
+ function createLaneData(displayName: string, isAutomated: boolean): LaneData {
33
+ return {
34
+ id: 1,
35
+ display_name: displayName,
36
+ visible: true,
37
+ count: 5,
38
+ sublanes: [
39
+ {
40
+ id: 2,
41
+ display_name: `Sublane of ${displayName}`,
42
+ visible: true,
43
+ count: 3,
44
+ sublanes: [],
45
+ custom_list_ids: [1],
46
+ inherit_parent_restrictions: false,
47
+ },
48
+ ],
49
+ // The absence/presence of custom list ids determines if a lane is automated or custom.
50
+ custom_list_ids: isAutomated ? [] : [1],
51
+ inherit_parent_restrictions: true,
52
+ };
53
+ }
54
+
55
+ describe("LaneEditor", () => {
56
+ describe("for a custom lane", () => {
57
+ const laneData = createLaneData("Custom Lane", false);
58
+
59
+ beforeEach(() => {
60
+ render(
61
+ <LaneEditor
62
+ library="library"
63
+ lane={laneData}
64
+ customLists={customListsData}
65
+ editOrCreate="edit"
66
+ editLane={editLane}
67
+ deleteLane={deleteLane}
68
+ findParentOfLane={stub().returns(laneData)}
69
+ toggleLaneVisibility={toggleLaneVisibility}
70
+ />
71
+ );
72
+ });
73
+
74
+ it("renders a delete button", () => {
75
+ expect(screen.getByRole("button", { name: /delete/i })).not.toBeNull();
76
+ });
77
+
78
+ it("renders an inherit parent restrictions checkbox", () => {
79
+ expect(
80
+ screen.getByRole("checkbox", {
81
+ name: /inherit restrictions from parent/i,
82
+ })
83
+ ).not.toBeNull();
84
+ });
85
+
86
+ it("renders a custom lists editor", () => {
87
+ expect(screen.getByTestId("LaneCustomListsEditor")).not.toBeNull();
88
+ });
89
+ });
90
+
91
+ describe("for an automated lane", () => {
92
+ const laneData = createLaneData("Automated Lane", true);
93
+
94
+ beforeEach(() => {
95
+ render(
96
+ <LaneEditor
97
+ library="library"
98
+ lane={laneData}
99
+ customLists={customListsData}
100
+ editOrCreate="edit"
101
+ editLane={editLane}
102
+ deleteLane={deleteLane}
103
+ findParentOfLane={stub().returns(laneData)}
104
+ toggleLaneVisibility={toggleLaneVisibility}
105
+ />
106
+ );
107
+ });
108
+
109
+ it("does not render a delete button", () => {
110
+ expect(screen.queryByRole("button", { name: /delete/i })).toBeNull();
111
+ });
112
+
113
+ it("does not render an inherit parent restrictions checkbox", () => {
114
+ expect(
115
+ screen.queryByRole("checkbox", {
116
+ name: /inherit restrictions from parent/i,
117
+ })
118
+ ).toBeNull();
119
+ });
120
+
121
+ it("does not render a custom lists editor", () => {
122
+ expect(screen.queryByTestId("LaneCustomListsEditor")).toBeNull();
123
+ });
124
+
125
+ it("renders an explanation that the lane contents can't be edited", () => {
126
+ expect(screen.getByText(/contents cannot be edited/i)).not.toBeNull();
127
+ });
128
+ });
129
+
130
+ it("doesn't render a custom lists editor while a lane is being loaded for editing", () => {
131
+ const laneData = null;
132
+
133
+ render(
134
+ <LaneEditor
135
+ library="library"
136
+ lane={laneData}
137
+ customLists={customListsData}
138
+ editOrCreate="edit"
139
+ editLane={editLane}
140
+ deleteLane={deleteLane}
141
+ findParentOfLane={stub().returns(laneData)}
142
+ toggleLaneVisibility={toggleLaneVisibility}
143
+ />
144
+ );
145
+
146
+ expect(screen.queryByTestId("LaneCustomListsEditor")).toBeNull();
147
+ });
148
+ });
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom";
@@ -0,0 +1,3 @@
1
+ test("adds 1 + 2 to equal 3", () => {
2
+ expect(1 + 2).toBe(3);
3
+ });
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import { render, RenderOptions, RenderResult } from "@testing-library/react";
3
+ import ContextProvider, {
4
+ ContextProviderProps,
5
+ } from "../../../src/components/ContextProvider";
6
+
7
+ /**
8
+ * Renders a given React element, wrapped in a ContextProvider. The resulting rerender function is
9
+ * also wrapped, so that rerenders will have the identical context.
10
+ *
11
+ * @param ui The element to render
12
+ * @param contextProviderProps Props to pass to the ContextProvider wrapper
13
+ * @param renderOptions Options to pass through to the RTL render function
14
+ * @returns
15
+ */
16
+ export default function renderWithContext(
17
+ ui: React.ReactElement,
18
+ contextProviderProps: ContextProviderProps,
19
+ renderOptions?: Omit<RenderOptions, "queries">
20
+ ): RenderResult {
21
+ const renderResult = render(
22
+ <ContextProvider {...contextProviderProps}>{ui}</ContextProvider>,
23
+ renderOptions
24
+ );
25
+
26
+ const rerenderWithContext = (ui) => {
27
+ return renderResult.rerender(
28
+ <ContextProvider {...contextProviderProps}>{ui}</ContextProvider>
29
+ );
30
+ };
31
+
32
+ return {
33
+ ...renderResult,
34
+ rerender: rerenderWithContext,
35
+ };
36
+ }
package/tsconfig.json CHANGED
@@ -5,17 +5,21 @@
5
5
  "target": "es5",
6
6
  "outDir": "lib",
7
7
  "lib": ["es2019", "dom"],
8
- "rootDir": "src",
9
- "types": ["mocha", "node"],
8
+ "rootDir": ".",
9
+ // Mocha and jest are both used in this project. Certain globals are declared by both,
10
+ // such as describe, it, and test. Setting skipLibCheck to true prevents typescript from
11
+ // erroring when it sees duplicate variable declarations with different types.
12
+ "skipLibCheck": true,
13
+ "types": ["mocha", "node", "jest", "@testing-library/jest-dom"],
10
14
  "jsx": "react"
11
15
  },
12
16
  "exclude": [
13
17
  "lib",
14
18
  "node_modules",
15
- "tests"
19
+ "tests/browser"
16
20
  ],
17
21
  "typedocOptions": {
18
- "mode": "file",
22
+ "entryPointStrategy": "expand",
19
23
  "out": "docs"
20
24
  }
21
25
  }
package/webpack.common.js CHANGED
@@ -3,7 +3,6 @@ const path = require("path");
3
3
  const CleanWebpackPlugin = require("clean-webpack-plugin");
4
4
  const webpack = require("webpack");
5
5
  const MiniCssExtractPlugin = require("mini-css-extract-plugin");
6
- const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
7
6
  const TerserPlugin = require("terser-webpack-plugin");
8
7
  const CopyPlugin = require("copy-webpack-plugin");
9
8
  /* eslint-enable @typescript-eslint/no-var-requires */
@@ -22,7 +21,7 @@ module.exports = {
22
21
  new CleanWebpackPlugin(),
23
22
  // jsdom is needed for server rendering, but causes errors
24
23
  // in the browser even if it is never used, so we ignore it:
25
- new webpack.IgnorePlugin(/jsdom$/),
24
+ new webpack.IgnorePlugin({ resourceRegExp: /jsdom$/ }),
26
25
  // Extract separate css file.
27
26
  new MiniCssExtractPlugin({ filename: "circulation-admin.css" }),
28
27
  // Set a local global variable in the app that will be used only
@@ -47,18 +46,23 @@ module.exports = {
47
46
  {
48
47
  test: /\.tsx?$/,
49
48
  exclude: [/node_modules/],
50
- loaders: ["ts-loader"],
49
+ use: ["ts-loader"],
51
50
  },
52
51
  {
53
52
  test: /\.(ttf|woff|eot|svg|png|woff2|gif|jpg)(\?[\s\S]+)?$/,
54
- loader: "url-loader?limit=100000",
53
+ use: ["url-loader?limit=100000"],
55
54
  },
56
55
  ],
57
56
  },
58
57
  resolve: {
59
- extensions: [".ts", ".tsx", ".js", ".scss"],
60
58
  alias: {
61
59
  react: path.resolve("./node_modules/react"),
62
60
  },
61
+ extensions: [".ts", ".tsx", ".js", ".scss"],
62
+ fallback: {
63
+ "stream": require.resolve("stream-browserify"),
64
+ "timers": require.resolve("timers-browserify"),
65
+ "url": require.resolve("url")
66
+ }
63
67
  },
64
68
  };
@@ -18,7 +18,7 @@ const {
18
18
  responseInterceptor,
19
19
  } = require("http-proxy-middleware");
20
20
 
21
- const merge = require("webpack-merge");
21
+ const { merge } = require("webpack-merge");
22
22
  const { URL } = require("url");
23
23
  const dev = require("./webpack.dev.config.js");
24
24
 
@@ -1,4 +1,4 @@
1
- const merge = require('webpack-merge');
1
+ const { merge } = require('webpack-merge');
2
2
  const common = require('./webpack.common.js');
3
3
 
4
4
  var config = merge(common, {
@@ -1,4 +1,4 @@
1
- const merge = require('webpack-merge');
1
+ const { merge } = require('webpack-merge');
2
2
  const common = require('./webpack.common.js');
3
3
 
4
4
  var config = merge(common, {