@thepalaceproject/circulation-admin 1.43.0-post.17 → 1.43.0-post.18
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 +1 -1
- package/tests/jest/components/AdvancedSearchBuilder.test.tsx +111 -0
- package/tests/jest/components/BookDetailsTabContainer.test.tsx +237 -0
- package/tests/jest/components/CatalogServices.test.tsx +38 -1
- package/tests/jest/components/ConfigTabContainer.test.tsx +196 -0
- package/tests/jest/components/CustomListsForBook.test.tsx +33 -0
- package/tests/jest/components/Libraries.test.tsx +48 -0
- package/tests/jest/components/MetadataServices.test.tsx +37 -1
- package/tests/jest/components/SelfTestsTabContainer.test.tsx +216 -0
package/package.json
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// AdvancedSearchBuilder is a pure forwarding component: it renders an
|
|
5
|
+
// AdvancedSearchFilterInput and an AdvancedSearchFilterViewer and threads their
|
|
6
|
+
// callbacks up to the builder's handlers, prefixed with the builder `name`. Mock
|
|
7
|
+
// both children to markers that capture the props they receive, so those
|
|
8
|
+
// forwarding closures can be exercised directly — including `onMove`, which is
|
|
9
|
+
// otherwise only reachable through a real react-dnd drag that jsdom can't perform.
|
|
10
|
+
let inputProps: any;
|
|
11
|
+
let viewerProps: any;
|
|
12
|
+
jest.mock("../../../src/components/AdvancedSearchFilterInput", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: (props: any) => {
|
|
15
|
+
inputProps = props;
|
|
16
|
+
return <div data-testid="filter-input" data-name={props.name} />;
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
jest.mock("../../../src/components/AdvancedSearchFilterViewer", () => ({
|
|
20
|
+
__esModule: true,
|
|
21
|
+
default: (props: any) => {
|
|
22
|
+
viewerProps = props;
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
data-testid="filter-viewer"
|
|
26
|
+
data-read-only={String(props.readOnly)}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import AdvancedSearchBuilder from "../../../src/components/AdvancedSearchBuilder";
|
|
33
|
+
|
|
34
|
+
describe("AdvancedSearchBuilder", () => {
|
|
35
|
+
const query = { id: "0", key: "genre", value: "Horror" };
|
|
36
|
+
|
|
37
|
+
const renderBuilder = (overrides: Record<string, unknown> = {}) => {
|
|
38
|
+
const handlers = {
|
|
39
|
+
addQuery: jest.fn(),
|
|
40
|
+
updateClearFiltersFlag: jest.fn(),
|
|
41
|
+
updateQueryBoolean: jest.fn(),
|
|
42
|
+
moveQuery: jest.fn(),
|
|
43
|
+
removeQuery: jest.fn(),
|
|
44
|
+
selectQuery: jest.fn(),
|
|
45
|
+
};
|
|
46
|
+
render(
|
|
47
|
+
<AdvancedSearchBuilder
|
|
48
|
+
isOwner
|
|
49
|
+
name="include"
|
|
50
|
+
query={query as any}
|
|
51
|
+
selectedQueryId="0"
|
|
52
|
+
{...handlers}
|
|
53
|
+
{...overrides}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
return handlers;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
inputProps = undefined;
|
|
61
|
+
viewerProps = undefined;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders the filter input and the viewer", () => {
|
|
65
|
+
renderBuilder();
|
|
66
|
+
|
|
67
|
+
expect(screen.getByTestId("filter-input")).toHaveAttribute(
|
|
68
|
+
"data-name",
|
|
69
|
+
"include"
|
|
70
|
+
);
|
|
71
|
+
expect(screen.getByTestId("filter-viewer")).toBeInTheDocument();
|
|
72
|
+
expect(viewerProps.query).toBe(query);
|
|
73
|
+
expect(viewerProps.selectedQueryId).toBe("0");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("hides the filter input and marks the viewer read-only when not the owner", () => {
|
|
77
|
+
renderBuilder({ isOwner: false });
|
|
78
|
+
|
|
79
|
+
expect(screen.queryByTestId("filter-input")).not.toBeInTheDocument();
|
|
80
|
+
expect(screen.getByTestId("filter-viewer")).toHaveAttribute(
|
|
81
|
+
"data-read-only",
|
|
82
|
+
"true"
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("forwards the input's onAdd and clear-filters callbacks with the builder name", () => {
|
|
87
|
+
const { addQuery, updateClearFiltersFlag } = renderBuilder();
|
|
88
|
+
|
|
89
|
+
const newQuery = { key: "title", value: "The Shining" };
|
|
90
|
+
inputProps.onAdd(newQuery);
|
|
91
|
+
inputProps.onClearFiltersFlagChange(true);
|
|
92
|
+
|
|
93
|
+
expect(addQuery).toHaveBeenCalledWith("include", newQuery);
|
|
94
|
+
expect(updateClearFiltersFlag).toHaveBeenCalledWith("include", true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("forwards the viewer's callbacks to their handlers with the builder name", () => {
|
|
98
|
+
const { updateQueryBoolean, moveQuery, removeQuery, selectQuery } =
|
|
99
|
+
renderBuilder();
|
|
100
|
+
|
|
101
|
+
viewerProps.onBooleanChange("2", "or");
|
|
102
|
+
viewerProps.onMove("2", "33");
|
|
103
|
+
viewerProps.onRemove("2");
|
|
104
|
+
viewerProps.onSelect("2");
|
|
105
|
+
|
|
106
|
+
expect(updateQueryBoolean).toHaveBeenCalledWith("include", "2", "or");
|
|
107
|
+
expect(moveQuery).toHaveBeenCalledWith("include", "2", "33");
|
|
108
|
+
expect(removeQuery).toHaveBeenCalledWith("include", "2");
|
|
109
|
+
expect(selectQuery).toHaveBeenCalledWith("include", "2");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { Provider } from "react-redux";
|
|
4
|
+
import { render, screen } from "@testing-library/react";
|
|
5
|
+
import userEvent from "@testing-library/user-event";
|
|
6
|
+
|
|
7
|
+
import buildStore from "../../../src/store";
|
|
8
|
+
import { getBookData } from "../../../src/features/book/bookEditorSlice";
|
|
9
|
+
|
|
10
|
+
// Each tab panel is a connected component that fetches over the network on mount.
|
|
11
|
+
// This container's behavior is the tab SET, tab SWITCHING, and clearing book data,
|
|
12
|
+
// not the panels' internals, so mock each panel to a marker echoing the props the
|
|
13
|
+
// container is responsible for threading into it.
|
|
14
|
+
jest.mock("../../../src/components/BookDetailsEditor", () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: (props: any) => (
|
|
17
|
+
<div
|
|
18
|
+
data-testid="book-details-editor"
|
|
19
|
+
data-csrf-token={props.csrfToken}
|
|
20
|
+
data-book-url={props.bookUrl}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
jest.mock("../../../src/components/Classifications", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: (props: any) => (
|
|
27
|
+
<div data-testid="classifications" data-book-url={props.bookUrl} />
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
jest.mock("../../../src/components/CustomListsForBook", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: (props: any) => (
|
|
33
|
+
<div
|
|
34
|
+
data-testid="custom-lists-for-book"
|
|
35
|
+
data-book-url={props.bookUrl}
|
|
36
|
+
data-library={props.library}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
}));
|
|
40
|
+
jest.mock("../../../src/components/BookCoverEditor", () => ({
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: (props: any) => (
|
|
43
|
+
<div data-testid="book-cover-editor" data-book-url={props.bookUrl} />
|
|
44
|
+
),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
import BookDetailsTabContainerConnected, {
|
|
48
|
+
BookDetailsTabContainer,
|
|
49
|
+
} from "../../../src/components/BookDetailsTabContainer";
|
|
50
|
+
|
|
51
|
+
// The container reads `router` and `pathFor` from the legacy React context the
|
|
52
|
+
// app supplies in production. Wrap it in a small provider that hands it those.
|
|
53
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
54
|
+
static childContextTypes = {
|
|
55
|
+
router: PropTypes.object,
|
|
56
|
+
pathFor: PropTypes.func,
|
|
57
|
+
};
|
|
58
|
+
getChildContext() {
|
|
59
|
+
return this.props.context;
|
|
60
|
+
}
|
|
61
|
+
render() {
|
|
62
|
+
return this.props.children;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TEST_BOOK_URL = "book url";
|
|
67
|
+
const TEST_COLLECTION_URL = "collection url";
|
|
68
|
+
|
|
69
|
+
describe("BookDetailsTabContainer", () => {
|
|
70
|
+
const pathFor = (collectionUrl: string, bookUrl: string, tab?: string) =>
|
|
71
|
+
`${collectionUrl}::${bookUrl}::${tab}`;
|
|
72
|
+
|
|
73
|
+
type Overrides = {
|
|
74
|
+
bookData?: any;
|
|
75
|
+
bookUrl?: string;
|
|
76
|
+
clearBook?: jest.Mock;
|
|
77
|
+
push?: jest.Mock;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const renderContainer = (overrides: Overrides = {}) => {
|
|
81
|
+
const push = overrides.push || jest.fn();
|
|
82
|
+
const clearBook = overrides.clearBook || jest.fn();
|
|
83
|
+
const context = { router: { push }, pathFor };
|
|
84
|
+
// One store for the lifetime of the render: a fresh one per element() call
|
|
85
|
+
// would hand the panels a new store identity on every rerender.
|
|
86
|
+
const store = buildStore();
|
|
87
|
+
const element = (bookUrl: string, bookData: any) => (
|
|
88
|
+
<LegacyContextProvider context={context}>
|
|
89
|
+
<BookDetailsTabContainer
|
|
90
|
+
bookUrl={bookUrl}
|
|
91
|
+
collectionUrl={TEST_COLLECTION_URL}
|
|
92
|
+
csrfToken="token"
|
|
93
|
+
refreshCatalog={jest.fn()}
|
|
94
|
+
store={store}
|
|
95
|
+
library={() => "library"}
|
|
96
|
+
canSuppress={true}
|
|
97
|
+
complaintsCount={0}
|
|
98
|
+
bookData={bookData}
|
|
99
|
+
clearBook={clearBook}
|
|
100
|
+
tab={null}
|
|
101
|
+
>
|
|
102
|
+
<div className="bookDetails">Moby Dick</div>
|
|
103
|
+
</BookDetailsTabContainer>
|
|
104
|
+
</LegacyContextProvider>
|
|
105
|
+
);
|
|
106
|
+
const result = render(
|
|
107
|
+
element(overrides.bookUrl ?? TEST_BOOK_URL, overrides.bookData ?? null)
|
|
108
|
+
);
|
|
109
|
+
const rerenderWith = (bookUrl: string, bookData: any = null) =>
|
|
110
|
+
result.rerender(element(bookUrl, bookData));
|
|
111
|
+
return { ...result, push, clearBook, rerenderWith };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
it("shows book details", () => {
|
|
115
|
+
renderContainer();
|
|
116
|
+
expect(screen.getByText("Moby Dick")).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("shows details, edit, and classifications tabs", () => {
|
|
120
|
+
renderContainer();
|
|
121
|
+
for (const name of ["Details", "Edit", "Classifications"]) {
|
|
122
|
+
expect(screen.getByRole("link", { name })).toBeInTheDocument();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("only shows cover tab when the book data has a change cover link", () => {
|
|
127
|
+
const { rerenderWith } = renderContainer();
|
|
128
|
+
expect(
|
|
129
|
+
screen.queryByRole("link", { name: "Cover" })
|
|
130
|
+
).not.toBeInTheDocument();
|
|
131
|
+
|
|
132
|
+
rerenderWith(TEST_BOOK_URL, {
|
|
133
|
+
title: "title",
|
|
134
|
+
changeCoverLink: {
|
|
135
|
+
href: "/change-cover",
|
|
136
|
+
rel: "http://librarysimplified.org/terms/rel/change_cover",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
expect(screen.getByRole("link", { name: "Cover" })).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("shows editor", () => {
|
|
143
|
+
renderContainer();
|
|
144
|
+
const editor = screen.getByTestId("book-details-editor");
|
|
145
|
+
expect(editor).toHaveAttribute("data-csrf-token", "token");
|
|
146
|
+
expect(editor).toHaveAttribute("data-book-url", TEST_BOOK_URL);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("shows classifications", () => {
|
|
150
|
+
renderContainer();
|
|
151
|
+
expect(screen.getByTestId("classifications")).toHaveAttribute(
|
|
152
|
+
"data-book-url",
|
|
153
|
+
TEST_BOOK_URL
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("shows lists", () => {
|
|
158
|
+
renderContainer();
|
|
159
|
+
const lists = screen.getByTestId("custom-lists-for-book");
|
|
160
|
+
expect(lists).toHaveAttribute("data-book-url", TEST_BOOK_URL);
|
|
161
|
+
expect(lists).toHaveAttribute("data-library", "library");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
165
|
+
const { push } = renderContainer();
|
|
166
|
+
await userEvent.click(screen.getByRole("link", { name: "Edit" }));
|
|
167
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(push).toHaveBeenCalledWith(
|
|
169
|
+
pathFor(TEST_COLLECTION_URL, TEST_BOOK_URL, "edit")
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("clears book data when receiving new book url", () => {
|
|
174
|
+
const clearBook = jest.fn();
|
|
175
|
+
const { rerenderWith } = renderContainer({ clearBook });
|
|
176
|
+
rerenderWith("new book url");
|
|
177
|
+
expect(clearBook).toHaveBeenCalledTimes(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("clears book data on unmount", () => {
|
|
181
|
+
const clearBook = jest.fn();
|
|
182
|
+
const { unmount } = renderContainer({ clearBook });
|
|
183
|
+
unmount();
|
|
184
|
+
expect(clearBook).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Render the CONNECTED default export so mapStateToProps / mapDispatchToProps
|
|
188
|
+
// (which run only when the connected component mounts) are exercised.
|
|
189
|
+
const renderConnected = (store = buildStore()) =>
|
|
190
|
+
render(
|
|
191
|
+
<Provider store={store}>
|
|
192
|
+
<LegacyContextProvider
|
|
193
|
+
context={{ router: { push: jest.fn() }, pathFor }}
|
|
194
|
+
>
|
|
195
|
+
<BookDetailsTabContainerConnected
|
|
196
|
+
bookUrl={TEST_BOOK_URL}
|
|
197
|
+
collectionUrl={TEST_COLLECTION_URL}
|
|
198
|
+
csrfToken="token"
|
|
199
|
+
refreshCatalog={jest.fn()}
|
|
200
|
+
library={() => "library"}
|
|
201
|
+
canSuppress={true}
|
|
202
|
+
tab={null}
|
|
203
|
+
>
|
|
204
|
+
<div className="bookDetails">Moby Dick</div>
|
|
205
|
+
</BookDetailsTabContainerConnected>
|
|
206
|
+
</LegacyContextProvider>
|
|
207
|
+
</Provider>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
it("wires up store and dispatch when connected", () => {
|
|
211
|
+
renderConnected();
|
|
212
|
+
expect(screen.getByRole("link", { name: "Details" })).toBeInTheDocument();
|
|
213
|
+
expect(screen.getByText("Moby Dick")).toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("takes book data from the store when connected", () => {
|
|
217
|
+
const store = buildStore();
|
|
218
|
+
store.dispatch(
|
|
219
|
+
getBookData.fulfilled(
|
|
220
|
+
{
|
|
221
|
+
title: "title",
|
|
222
|
+
changeCoverLink: {
|
|
223
|
+
href: "/change-cover",
|
|
224
|
+
rel: "http://librarysimplified.org/terms/rel/change_cover",
|
|
225
|
+
},
|
|
226
|
+
} as any,
|
|
227
|
+
"request-id",
|
|
228
|
+
{ url: TEST_BOOK_URL } as any
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
renderConnected(store);
|
|
232
|
+
// The cover tab appears only when bookData carries a changeCoverLink, so it
|
|
233
|
+
// is the observable proof that mapStateToProps threaded the store's book
|
|
234
|
+
// data into the container.
|
|
235
|
+
expect(screen.getByRole("link", { name: "Cover" })).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { screen, within } from "@testing-library/react";
|
|
3
|
-
import {
|
|
3
|
+
import CatalogServicesConnected, {
|
|
4
|
+
CatalogServices,
|
|
5
|
+
} from "../../../src/components/CatalogServices";
|
|
4
6
|
import renderWithContext from "../testUtils/renderWithContext";
|
|
7
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
8
|
+
import buildStore from "../../../src/store";
|
|
5
9
|
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
6
10
|
|
|
7
11
|
describe("CatalogServices", () => {
|
|
@@ -61,3 +65,36 @@ describe("CatalogServices", () => {
|
|
|
61
65
|
);
|
|
62
66
|
});
|
|
63
67
|
});
|
|
68
|
+
|
|
69
|
+
describe("CatalogServices - connected wiring", () => {
|
|
70
|
+
afterEach(() => jest.restoreAllMocks());
|
|
71
|
+
|
|
72
|
+
it("wires mapStateToProps/mapDispatchToProps and renders the fetched list", async () => {
|
|
73
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
74
|
+
async () =>
|
|
75
|
+
new Response(
|
|
76
|
+
JSON.stringify({
|
|
77
|
+
catalog_services: [
|
|
78
|
+
{ id: 2, protocol: "test protocol", name: "Test Catalog" },
|
|
79
|
+
],
|
|
80
|
+
protocols: [{ name: "test protocol", label: "TP", settings: [] }],
|
|
81
|
+
}),
|
|
82
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
const store = buildStore();
|
|
86
|
+
|
|
87
|
+
renderWithProviders(<CatalogServicesConnected csrfToken="token" />, {
|
|
88
|
+
reduxProviderProps: { store },
|
|
89
|
+
appConfigSettings: { roles: [{ role: "system" }] },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// The edit link only appears once the on-mount fetch resolves and
|
|
93
|
+
// mapStateToProps feeds the fetched service back in as props.
|
|
94
|
+
const editLink = await screen.findByRole("link", { name: /edit/i });
|
|
95
|
+
expect(editLink).toHaveAttribute(
|
|
96
|
+
"href",
|
|
97
|
+
"/admin/web/config/catalogServices/edit/2"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
import Admin from "../../../src/models/Admin";
|
|
8
|
+
|
|
9
|
+
// Every configuration tab panel is a connected component that fetches on mount.
|
|
10
|
+
// This container's behavior is the tab SET and tab SWITCHING, not the panels'
|
|
11
|
+
// internals, so mock each panel to a marker echoing the props the container is
|
|
12
|
+
// responsible for threading into it (csrfToken / editOrCreate / identifier).
|
|
13
|
+
const panelMock = (testId: string) => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: (props: any) => (
|
|
16
|
+
<div
|
|
17
|
+
data-testid={testId}
|
|
18
|
+
data-csrf-token={props.csrfToken}
|
|
19
|
+
data-edit-or-create={props.editOrCreate}
|
|
20
|
+
data-identifier={props.identifier}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
jest.mock("../../../src/components/Libraries", () => panelMock("libraries"));
|
|
26
|
+
jest.mock("../../../src/components/Collections", () =>
|
|
27
|
+
panelMock("collections")
|
|
28
|
+
);
|
|
29
|
+
jest.mock("../../../src/components/IndividualAdmins", () =>
|
|
30
|
+
panelMock("individual-admins")
|
|
31
|
+
);
|
|
32
|
+
jest.mock("../../../src/components/PatronAuthServices", () =>
|
|
33
|
+
panelMock("patron-auth-services")
|
|
34
|
+
);
|
|
35
|
+
jest.mock("../../../src/components/MetadataServices", () =>
|
|
36
|
+
panelMock("metadata-services")
|
|
37
|
+
);
|
|
38
|
+
jest.mock("../../../src/components/CatalogServices", () =>
|
|
39
|
+
panelMock("catalog-services")
|
|
40
|
+
);
|
|
41
|
+
jest.mock("../../../src/components/DiscoveryServices", () =>
|
|
42
|
+
panelMock("discovery-services")
|
|
43
|
+
);
|
|
44
|
+
jest.mock("../../../src/components/SitewideAnnouncements", () =>
|
|
45
|
+
panelMock("sitewide-announcements")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
import ConfigTabContainer from "../../../src/components/ConfigTabContainer";
|
|
49
|
+
|
|
50
|
+
// ConfigTabContainer reads `router`, `pathFor`, and `admin` from the legacy React
|
|
51
|
+
// context the app supplies in production. Wrap it in a small provider that hands
|
|
52
|
+
// it those.
|
|
53
|
+
class LegacyContextProvider extends React.Component<{ context: any }> {
|
|
54
|
+
static childContextTypes = {
|
|
55
|
+
router: PropTypes.object,
|
|
56
|
+
pathFor: PropTypes.func,
|
|
57
|
+
admin: PropTypes.object,
|
|
58
|
+
};
|
|
59
|
+
getChildContext() {
|
|
60
|
+
return this.props.context;
|
|
61
|
+
}
|
|
62
|
+
render() {
|
|
63
|
+
return this.props.children;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("ConfigTabContainer", () => {
|
|
68
|
+
const systemAdmin = new Admin([{ role: "system" }]);
|
|
69
|
+
const libraryManagerA = new Admin([{ role: "manager", library: "a" }]);
|
|
70
|
+
const sitewideLibrarian = new Admin([{ role: "librarian-all" }]);
|
|
71
|
+
|
|
72
|
+
const renderWithAdmin = (admin: Admin, push: jest.Mock = jest.fn()) => {
|
|
73
|
+
const context = { router: { push }, pathFor: jest.fn(), admin };
|
|
74
|
+
const result = render(
|
|
75
|
+
<LegacyContextProvider context={context}>
|
|
76
|
+
<ConfigTabContainer
|
|
77
|
+
tab={null}
|
|
78
|
+
csrfToken="token"
|
|
79
|
+
store={buildStore()}
|
|
80
|
+
editOrCreate="edit"
|
|
81
|
+
identifier="identifier"
|
|
82
|
+
/>
|
|
83
|
+
</LegacyContextProvider>
|
|
84
|
+
);
|
|
85
|
+
return { ...result, push };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const expectPanelProps = (testId: string) => {
|
|
89
|
+
const panel = screen.getByTestId(testId);
|
|
90
|
+
expect(panel).toHaveAttribute("data-csrf-token", "token");
|
|
91
|
+
expect(panel).toHaveAttribute("data-edit-or-create", "edit");
|
|
92
|
+
expect(panel).toHaveAttribute("data-identifier", "identifier");
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe("for system admin", () => {
|
|
96
|
+
it("shows tabs", () => {
|
|
97
|
+
renderWithAdmin(systemAdmin);
|
|
98
|
+
for (const name of [
|
|
99
|
+
"Libraries",
|
|
100
|
+
"Admins",
|
|
101
|
+
"Collections",
|
|
102
|
+
"Patron Authentication",
|
|
103
|
+
"Metadata",
|
|
104
|
+
"External Catalogs",
|
|
105
|
+
"Discovery",
|
|
106
|
+
"Sitewide Announcements",
|
|
107
|
+
]) {
|
|
108
|
+
expect(screen.getByRole("link", { name })).toBeInTheDocument();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("shows components", () => {
|
|
113
|
+
renderWithAdmin(systemAdmin);
|
|
114
|
+
for (const testId of [
|
|
115
|
+
"libraries",
|
|
116
|
+
"individual-admins",
|
|
117
|
+
"collections",
|
|
118
|
+
"patron-auth-services",
|
|
119
|
+
"metadata-services",
|
|
120
|
+
"catalog-services",
|
|
121
|
+
"discovery-services",
|
|
122
|
+
"sitewide-announcements",
|
|
123
|
+
]) {
|
|
124
|
+
expectPanelProps(testId);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("uses router to navigate when tab is clicked", async () => {
|
|
129
|
+
const { push } = renderWithAdmin(systemAdmin);
|
|
130
|
+
await userEvent.click(screen.getByRole("link", { name: "Collections" }));
|
|
131
|
+
expect(push).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(push).toHaveBeenCalledWith("/admin/web/config/collections");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("for library manager", () => {
|
|
137
|
+
it("shows tabs", () => {
|
|
138
|
+
renderWithAdmin(libraryManagerA);
|
|
139
|
+
expect(
|
|
140
|
+
screen.getByRole("link", { name: "Libraries" })
|
|
141
|
+
).toBeInTheDocument();
|
|
142
|
+
expect(screen.getByRole("link", { name: "Admins" })).toBeInTheDocument();
|
|
143
|
+
for (const name of [
|
|
144
|
+
"Collections",
|
|
145
|
+
"Patron Authentication",
|
|
146
|
+
"Metadata",
|
|
147
|
+
"External Catalogs",
|
|
148
|
+
]) {
|
|
149
|
+
expect(screen.queryByRole("link", { name })).not.toBeInTheDocument();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("shows components", () => {
|
|
154
|
+
renderWithAdmin(libraryManagerA);
|
|
155
|
+
expectPanelProps("libraries");
|
|
156
|
+
expectPanelProps("individual-admins");
|
|
157
|
+
for (const testId of [
|
|
158
|
+
"collections",
|
|
159
|
+
"patron-auth-services",
|
|
160
|
+
"metadata-services",
|
|
161
|
+
"catalog-services",
|
|
162
|
+
"discovery-services",
|
|
163
|
+
"sitewide-announcements",
|
|
164
|
+
]) {
|
|
165
|
+
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("for librarian", () => {
|
|
171
|
+
it("shows tabs", () => {
|
|
172
|
+
const { container } = renderWithAdmin(sitewideLibrarian);
|
|
173
|
+
const links = container.querySelectorAll("ul.nav-tabs a");
|
|
174
|
+
expect(links).toHaveLength(1);
|
|
175
|
+
expect(
|
|
176
|
+
screen.getByRole("link", { name: "Libraries" })
|
|
177
|
+
).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("shows components", () => {
|
|
181
|
+
renderWithAdmin(sitewideLibrarian);
|
|
182
|
+
expectPanelProps("libraries");
|
|
183
|
+
for (const testId of [
|
|
184
|
+
"individual-admins",
|
|
185
|
+
"collections",
|
|
186
|
+
"patron-auth-services",
|
|
187
|
+
"metadata-services",
|
|
188
|
+
"catalog-services",
|
|
189
|
+
"discovery-services",
|
|
190
|
+
"sitewide-announcements",
|
|
191
|
+
]) {
|
|
192
|
+
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -312,4 +312,37 @@ describe("CustomListsForBook", () => {
|
|
|
312
312
|
expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled();
|
|
313
313
|
expect(screen.getByRole("button", { name: "Add" })).toBeDisabled();
|
|
314
314
|
});
|
|
315
|
+
|
|
316
|
+
it("resyncs its lists when the book url changes", () => {
|
|
317
|
+
const commonProps = {
|
|
318
|
+
csrfToken: "token",
|
|
319
|
+
library: "library",
|
|
320
|
+
refreshCatalog: jest.fn().mockResolvedValue(undefined),
|
|
321
|
+
allCustomLists: allCustomLists as any,
|
|
322
|
+
fetchAllCustomLists: jest.fn(),
|
|
323
|
+
fetchCustomListsForBook: jest.fn(),
|
|
324
|
+
editCustomListsForBook: jest.fn().mockResolvedValue(undefined),
|
|
325
|
+
};
|
|
326
|
+
const { rerender } = render(
|
|
327
|
+
<UnconnectedCustomListsForBook
|
|
328
|
+
{...commonProps}
|
|
329
|
+
book={{ id: "a", title: "Book A" } as any}
|
|
330
|
+
bookUrl="works/book-a"
|
|
331
|
+
customListsForBook={listsForBook as any}
|
|
332
|
+
/>
|
|
333
|
+
);
|
|
334
|
+
// list 2 (the book's only list) is in the current column.
|
|
335
|
+
expect(screen.getByText("list 2")).toBeInTheDocument();
|
|
336
|
+
|
|
337
|
+
// A new book url re-syncs the current lists from the new customListsForBook.
|
|
338
|
+
rerender(
|
|
339
|
+
<UnconnectedCustomListsForBook
|
|
340
|
+
{...commonProps}
|
|
341
|
+
book={{ id: "b", title: "Book B" } as any}
|
|
342
|
+
bookUrl="works/book-b"
|
|
343
|
+
customListsForBook={[allCustomLists[0]] as any}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
expect(screen.getByText("list 1")).toBeInTheDocument();
|
|
347
|
+
});
|
|
315
348
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import Libraries from "../../../src/components/Libraries";
|
|
5
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
|
|
8
|
+
// The connected default export is what the app mounts, so render that rather
|
|
9
|
+
// than the bare component: it is the only thing that exercises Libraries'
|
|
10
|
+
// mapStateToProps/mapDispatchToProps.
|
|
11
|
+
describe("Libraries - connected wiring", () => {
|
|
12
|
+
afterEach(() => jest.restoreAllMocks());
|
|
13
|
+
|
|
14
|
+
it("wires mapStateToProps/mapDispatchToProps and renders the fetched list", async () => {
|
|
15
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
16
|
+
async () =>
|
|
17
|
+
new Response(
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
libraries: [
|
|
20
|
+
{
|
|
21
|
+
uuid: "uuid-nypl",
|
|
22
|
+
short_name: "nypl",
|
|
23
|
+
name: "New York Public Library",
|
|
24
|
+
settings: {},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
renderWithProviders(<Libraries csrfToken="token" />, {
|
|
33
|
+
reduxProviderProps: { store: buildStore() },
|
|
34
|
+
appConfigSettings: { roles: [{ role: "system" }] },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The library and its edit control appear once the on-mount fetch resolves
|
|
38
|
+
// and mapStateToProps feeds the fetched data back in as props.
|
|
39
|
+
expect(
|
|
40
|
+
await screen.findByText("New York Public Library")
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
const editLink = await screen.findByRole("link", { name: /edit/i });
|
|
43
|
+
expect(editLink).toHaveAttribute(
|
|
44
|
+
"href",
|
|
45
|
+
"/admin/web/config/libraries/edit/uuid-nypl"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { screen, within } from "@testing-library/react";
|
|
3
|
-
import {
|
|
3
|
+
import MetadataServicesConnected, {
|
|
4
|
+
MetadataServices,
|
|
5
|
+
} from "../../../src/components/MetadataServices";
|
|
4
6
|
import renderWithContext from "../testUtils/renderWithContext";
|
|
7
|
+
import { renderWithProviders } from "../testUtils/withProviders";
|
|
8
|
+
import buildStore from "../../../src/store";
|
|
5
9
|
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
|
|
6
10
|
|
|
7
11
|
describe("MetadataServices", () => {
|
|
@@ -57,3 +61,35 @@ describe("MetadataServices", () => {
|
|
|
57
61
|
);
|
|
58
62
|
});
|
|
59
63
|
});
|
|
64
|
+
|
|
65
|
+
describe("MetadataServices - connected wiring", () => {
|
|
66
|
+
afterEach(() => jest.restoreAllMocks());
|
|
67
|
+
|
|
68
|
+
it("wires mapStateToProps/mapDispatchToProps and renders the fetched list", async () => {
|
|
69
|
+
jest.spyOn(globalThis, "fetch").mockImplementation(
|
|
70
|
+
async () =>
|
|
71
|
+
new Response(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
metadata_services: [
|
|
74
|
+
{ id: 2, protocol: "test protocol", name: "Test Metadata" },
|
|
75
|
+
],
|
|
76
|
+
protocols: [{ name: "test protocol", label: "TP", settings: [] }],
|
|
77
|
+
}),
|
|
78
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
renderWithProviders(<MetadataServicesConnected csrfToken="token" />, {
|
|
83
|
+
reduxProviderProps: { store: buildStore() },
|
|
84
|
+
appConfigSettings: { roles: [{ role: "system" }] },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// The edit link only appears once the on-mount fetch resolves and
|
|
88
|
+
// mapStateToProps feeds the fetched service back in as props.
|
|
89
|
+
const editLink = await screen.findByRole("link", { name: /edit/i });
|
|
90
|
+
expect(editLink).toHaveAttribute(
|
|
91
|
+
"href",
|
|
92
|
+
"/admin/web/config/metadata/edit/2"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as PropTypes from "prop-types";
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import buildStore from "../../../src/store";
|
|
7
|
+
|
|
8
|
+
// Each active tab renders the connected SelfTestsCategory, which fetches on mount
|
|
9
|
+
// and recurses into the self-tests machinery. This container's behavior is which
|
|
10
|
+
// slice of the store each category gets and under what type/link name, so mock it
|
|
11
|
+
// to a marker echoing those. `data-items` names the slice's contents so passing
|
|
12
|
+
// the wrong category's items — or the whole items object — is visible.
|
|
13
|
+
jest.mock("../../../src/components/SelfTestsCategory", () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: (props: any) => (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="self-tests-category"
|
|
18
|
+
data-type={props.type}
|
|
19
|
+
data-link-name={props.linkName}
|
|
20
|
+
data-items={
|
|
21
|
+
Array.isArray(props.items)
|
|
22
|
+
? props.items.map((item: any) => item.name).join(",")
|
|
23
|
+
: `not-an-array:${typeof props.items}`
|
|
24
|
+
}
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { SelfTestsTabContainer } from "../../../src/components/SelfTestsTabContainer";
|
|
30
|
+
|
|
31
|
+
// SelfTestsTabContainer reads `router` and `pathFor` from legacy react-router
|
|
32
|
+
// context via static contextTypes (handleSelect calls router.push). Supply them
|
|
33
|
+
// through a tiny legacy-context provider.
|
|
34
|
+
class LegacyContextProvider extends React.Component<{
|
|
35
|
+
router: { push: jest.Mock };
|
|
36
|
+
pathFor: jest.Mock;
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
}> {
|
|
39
|
+
static childContextTypes = {
|
|
40
|
+
router: PropTypes.object,
|
|
41
|
+
pathFor: PropTypes.func,
|
|
42
|
+
};
|
|
43
|
+
getChildContext() {
|
|
44
|
+
return { router: this.props.router, pathFor: this.props.pathFor };
|
|
45
|
+
}
|
|
46
|
+
render() {
|
|
47
|
+
return <>{this.props.children}</>;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("SelfTestsTabContainer", () => {
|
|
52
|
+
let goToTab: jest.Mock;
|
|
53
|
+
let fetchItems: jest.Mock;
|
|
54
|
+
let router: { push: jest.Mock };
|
|
55
|
+
let pathFor: jest.Mock;
|
|
56
|
+
let store;
|
|
57
|
+
|
|
58
|
+
const collections = [
|
|
59
|
+
{
|
|
60
|
+
id: 1,
|
|
61
|
+
name: "collection 1",
|
|
62
|
+
protocol: "protocol",
|
|
63
|
+
self_test_results: {
|
|
64
|
+
duration: 1.75,
|
|
65
|
+
start: "2018-08-07T19:34:54Z",
|
|
66
|
+
end: "2018-08-07T19:34:55Z",
|
|
67
|
+
results: [
|
|
68
|
+
{
|
|
69
|
+
duration: 0.000119,
|
|
70
|
+
end: "2018-08-07T19:34:54Z",
|
|
71
|
+
exception: null,
|
|
72
|
+
name: "Initial setup.",
|
|
73
|
+
result: null,
|
|
74
|
+
start: "2018-08-07T19:34:54Z",
|
|
75
|
+
success: true,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
store = buildStore();
|
|
84
|
+
goToTab = jest.fn();
|
|
85
|
+
fetchItems = jest.fn();
|
|
86
|
+
router = { push: jest.fn() };
|
|
87
|
+
pathFor = jest.fn();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const element = (overrides = {}) => {
|
|
91
|
+
const props = {
|
|
92
|
+
store,
|
|
93
|
+
goToTab,
|
|
94
|
+
fetchItems,
|
|
95
|
+
tab: "collections",
|
|
96
|
+
items: { protocols: [], collections },
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
return (
|
|
100
|
+
<LegacyContextProvider router={router} pathFor={pathFor}>
|
|
101
|
+
<SelfTestsTabContainer {...props} />
|
|
102
|
+
</LegacyContextProvider>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const renderContainer = (overrides = {}) => render(element(overrides));
|
|
107
|
+
|
|
108
|
+
it("renders a tab container", () => {
|
|
109
|
+
const { container } = renderContainer();
|
|
110
|
+
expect(container.firstChild).toHaveClass("tab-container");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("renders tabs and defaults to showing the Collections tab", () => {
|
|
114
|
+
const { container } = renderContainer();
|
|
115
|
+
const tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
116
|
+
expect(tabs).toHaveLength(3);
|
|
117
|
+
|
|
118
|
+
expect(tabs[0]).toHaveTextContent("Collections");
|
|
119
|
+
expect(tabs[0]).toHaveClass("active");
|
|
120
|
+
|
|
121
|
+
expect(tabs[1]).toHaveTextContent("Patron Authentication");
|
|
122
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
123
|
+
|
|
124
|
+
expect(tabs[2]).toHaveTextContent("Metadata Services");
|
|
125
|
+
expect(tabs[2]).not.toHaveClass("active");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("renders tab content", () => {
|
|
129
|
+
const { container } = renderContainer();
|
|
130
|
+
|
|
131
|
+
// Only the collections data is present, so its category renders; the other
|
|
132
|
+
// two tabs fall back to a loading indicator.
|
|
133
|
+
const markers = screen.getAllByTestId("self-tests-category");
|
|
134
|
+
expect(markers).toHaveLength(1);
|
|
135
|
+
expect(markers[0]).toHaveAttribute("data-type", "collection");
|
|
136
|
+
expect(markers[0]).toHaveAttribute("data-link-name", "collections");
|
|
137
|
+
expect(markers[0]).toHaveAttribute("data-items", "collection 1");
|
|
138
|
+
|
|
139
|
+
expect(container.querySelectorAll(".loading")).toHaveLength(2);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("calls fetchItems on mount", () => {
|
|
143
|
+
renderContainer();
|
|
144
|
+
expect(fetchItems).toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("shows an error message", () => {
|
|
148
|
+
const { container, rerender } = renderContainer();
|
|
149
|
+
expect(container.querySelectorAll(".alert-danger")).toHaveLength(0);
|
|
150
|
+
|
|
151
|
+
rerender(
|
|
152
|
+
element({
|
|
153
|
+
fetchError: { status: 401, response: "error fetching diagnostics" },
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(container.querySelectorAll(".alert-danger")).toHaveLength(3);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("calls goToTab and pushes to the router when a tab is clicked", async () => {
|
|
161
|
+
renderContainer();
|
|
162
|
+
|
|
163
|
+
await userEvent.click(
|
|
164
|
+
screen.getByRole("link", { name: "Patron Authentication" })
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(goToTab).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(goToTab).toHaveBeenCalledWith("patronAuthServices");
|
|
169
|
+
await waitFor(() =>
|
|
170
|
+
expect(router.push).toHaveBeenCalledWith(
|
|
171
|
+
"/admin/web/troubleshooting/self-tests/patronAuthServices"
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("switches tabs when the tab prop changes", () => {
|
|
177
|
+
const { container, rerender } = renderContainer();
|
|
178
|
+
let tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
179
|
+
expect(tabs[0]).toHaveClass("active");
|
|
180
|
+
expect(tabs[1]).not.toHaveClass("active");
|
|
181
|
+
|
|
182
|
+
rerender(element({ tab: "patronAuthServices" }));
|
|
183
|
+
|
|
184
|
+
tabs = container.querySelectorAll("ul.nav-tabs li");
|
|
185
|
+
expect(tabs[0]).not.toHaveClass("active");
|
|
186
|
+
expect(tabs[1]).toHaveClass("active");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("derives the store key, type, and link name for each category", () => {
|
|
190
|
+
// A category only renders once its store key holds items, so populate all
|
|
191
|
+
// three with distinctly named items: the key each category is looked up
|
|
192
|
+
// under is then observable in the items its marker receives.
|
|
193
|
+
renderContainer({
|
|
194
|
+
items: {
|
|
195
|
+
collections: [{ id: 1, name: "a collection" }],
|
|
196
|
+
patron_auth_services: [{ id: 2, name: "a patron auth service" }],
|
|
197
|
+
metadata_services: [{ id: 3, name: "a metadata service" }],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const markers = screen.getAllByTestId("self-tests-category");
|
|
202
|
+
expect(markers).toHaveLength(3);
|
|
203
|
+
|
|
204
|
+
expect(markers[0]).toHaveAttribute("data-type", "collection");
|
|
205
|
+
expect(markers[0]).toHaveAttribute("data-link-name", "collections");
|
|
206
|
+
expect(markers[0]).toHaveAttribute("data-items", "a collection");
|
|
207
|
+
|
|
208
|
+
expect(markers[1]).toHaveAttribute("data-type", "patron_auth_service");
|
|
209
|
+
expect(markers[1]).toHaveAttribute("data-link-name", "patronAuth");
|
|
210
|
+
expect(markers[1]).toHaveAttribute("data-items", "a patron auth service");
|
|
211
|
+
|
|
212
|
+
expect(markers[2]).toHaveAttribute("data-type", "metadata_service");
|
|
213
|
+
expect(markers[2]).toHaveAttribute("data-link-name", "metadata");
|
|
214
|
+
expect(markers[2]).toHaveAttribute("data-items", "a metadata service");
|
|
215
|
+
});
|
|
216
|
+
});
|