Skip to content

Commit 188848b

Browse files
Merge pull request #712 from zendesk/khopek/PDSC-60-race-service-catalog-item
(fix:service-catalog): Service Catalog item is able to be saved with …
2 parents a692044 + bad48f8 commit 188848b

File tree

2 files changed

+377
-30
lines changed

2 files changed

+377
-30
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
2+
import { ThemeProvider } from "styled-components";
3+
import { DEFAULT_THEME } from "@zendeskgarden/react-theming";
4+
import { ServiceCatalogItem } from "./ServiceCatalogItem";
5+
import { submitServiceItemRequest } from "./submitServiceItemRequest";
6+
import * as notifications from "../../../shared/notifications";
7+
import type { ServiceCatalogItem as ServiceCatalogItemType } from "../../data-types/ServiceCatalogItem";
8+
import type { TicketFieldObject } from "../../../ticket-fields/data-types/TicketFieldObject";
9+
10+
// Mock react-i18next
11+
jest.mock("react-i18next", () => ({
12+
useTranslation: () => ({
13+
t: (key: string, defaultValue: string) => defaultValue,
14+
}),
15+
}));
16+
17+
// Mock the hooks
18+
jest.mock("../../hooks/useServiceCatalogItem", () => ({
19+
useServiceCatalogItem: jest.fn(),
20+
}));
21+
22+
jest.mock("../../hooks/useItemFormFields", () => ({
23+
useItemFormFields: jest.fn(),
24+
}));
25+
26+
// Mock submitServiceItemRequest
27+
jest.mock("./submitServiceItemRequest", () => ({
28+
submitServiceItemRequest: jest.fn(),
29+
}));
30+
31+
// Mock notifications
32+
jest.mock("../../../shared/notifications", () => ({
33+
notify: jest.fn(),
34+
addFlashNotification: jest.fn(),
35+
}));
36+
37+
// Mock ItemRequestForm to simplify testing
38+
jest.mock("./ItemRequestForm", () => ({
39+
ItemRequestForm: ({
40+
onSubmit,
41+
}: {
42+
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
43+
}) => (
44+
<form data-testid="item-request-form" onSubmit={onSubmit}>
45+
<button type="submit">Submit</button>
46+
</form>
47+
),
48+
ASSET_TYPE_KEY: "zen:custom_object:standard::itam_asset_type",
49+
}));
50+
51+
import { useServiceCatalogItem } from "../../hooks/useServiceCatalogItem";
52+
import { useItemFormFields } from "../../hooks/useItemFormFields";
53+
54+
const mockUseServiceCatalogItem = useServiceCatalogItem as jest.MockedFunction<
55+
typeof useServiceCatalogItem
56+
>;
57+
const mockUseItemFormFields = useItemFormFields as jest.MockedFunction<
58+
typeof useItemFormFields
59+
>;
60+
const mockSubmitServiceItemRequest =
61+
submitServiceItemRequest as jest.MockedFunction<
62+
typeof submitServiceItemRequest
63+
>;
64+
const mockNotify = notifications.notify as jest.MockedFunction<
65+
typeof notifications.notify
66+
>;
67+
68+
const renderWithTheme = (component: React.ReactElement) => {
69+
return render(
70+
<ThemeProvider theme={DEFAULT_THEME}>{component}</ThemeProvider>
71+
);
72+
};
73+
74+
describe("ServiceCatalogItem", () => {
75+
const defaultProps = {
76+
serviceCatalogItemId: 1,
77+
baseLocale: "en-us",
78+
hasAtMentions: false,
79+
userRole: "end_user",
80+
userId: 123,
81+
brandId: 456,
82+
organizations: [],
83+
helpCenterPath: "/hc/en-us",
84+
};
85+
86+
const mockServiceCatalogItem: ServiceCatalogItemType = {
87+
id: 1,
88+
name: "Test Service",
89+
description: "Test Description",
90+
form_id: 100,
91+
thumbnail_url: "",
92+
custom_object_fields: {
93+
"standard::asset_option": "",
94+
"standard::asset_type_option": "",
95+
},
96+
};
97+
98+
const mockRequestFields: TicketFieldObject[] = [
99+
{
100+
id: 1,
101+
name: "custom_fields_1",
102+
type: "text",
103+
description: "Field 1",
104+
label: "Field 1",
105+
required: true,
106+
options: [],
107+
value: null,
108+
error: null,
109+
},
110+
];
111+
112+
const mockAssociatedLookupField: TicketFieldObject = {
113+
id: 2,
114+
name: "custom_fields_2",
115+
type: "lookup",
116+
description: "Lookup",
117+
label: "Lookup",
118+
required: true,
119+
options: [],
120+
value: null,
121+
error: null,
122+
relationship_target_type: "standard::service_catalog_item",
123+
};
124+
125+
beforeEach(() => {
126+
jest.clearAllMocks();
127+
128+
mockUseServiceCatalogItem.mockReturnValue({
129+
serviceCatalogItem: mockServiceCatalogItem,
130+
errorFetchingItem: null,
131+
});
132+
133+
mockUseItemFormFields.mockReturnValue({
134+
requestFields: mockRequestFields,
135+
associatedLookupField: mockAssociatedLookupField,
136+
error: null,
137+
setRequestFields: jest.fn(),
138+
handleChange: jest.fn(),
139+
});
140+
});
141+
142+
describe("error handling on form submission", () => {
143+
it("should show error notification when 422 response has missing fields not in the form", async () => {
144+
const errorResponse = {
145+
ok: false,
146+
status: 422,
147+
json: () =>
148+
Promise.resolve({
149+
error: "RecordInvalid",
150+
description: "Record validation errors",
151+
details: {
152+
base: [
153+
{
154+
description: "Field is required",
155+
error: "BlankValue",
156+
field_key: 999, // Field not in the form
157+
},
158+
],
159+
},
160+
}),
161+
};
162+
163+
mockSubmitServiceItemRequest.mockResolvedValue(
164+
errorResponse as unknown as Response
165+
);
166+
167+
renderWithTheme(<ServiceCatalogItem {...defaultProps} />);
168+
169+
const form = screen.getByTestId("item-request-form");
170+
fireEvent.submit(form);
171+
172+
await waitFor(() => {
173+
expect(mockNotify).toHaveBeenCalledWith(
174+
expect.objectContaining({
175+
type: "error",
176+
title: "Service couldn't be submitted",
177+
})
178+
);
179+
});
180+
});
181+
182+
it("should show error notification when 422 response has field errors for fields in the form", async () => {
183+
const errorResponse = {
184+
ok: false,
185+
status: 422,
186+
json: () =>
187+
Promise.resolve({
188+
error: "RecordInvalid",
189+
description: "Record validation errors",
190+
details: {
191+
base: [
192+
{
193+
description: "Field is required",
194+
error: "BlankValue",
195+
field_key: 1, // Field IS in the form
196+
},
197+
],
198+
},
199+
}),
200+
};
201+
202+
mockSubmitServiceItemRequest.mockResolvedValue(
203+
errorResponse as unknown as Response
204+
);
205+
206+
renderWithTheme(<ServiceCatalogItem {...defaultProps} />);
207+
208+
const form = screen.getByTestId("item-request-form");
209+
fireEvent.submit(form);
210+
211+
await waitFor(() => {
212+
expect(mockNotify).toHaveBeenCalledWith(
213+
expect.objectContaining({
214+
type: "error",
215+
title: "Service couldn't be submitted",
216+
message: "Give it a moment and try it again",
217+
})
218+
);
219+
});
220+
});
221+
222+
it("should show error notification when 422 response JSON parsing fails", async () => {
223+
const errorResponse = {
224+
ok: false,
225+
status: 422,
226+
json: () => Promise.reject(new Error("Invalid JSON")),
227+
};
228+
229+
mockSubmitServiceItemRequest.mockResolvedValue(
230+
errorResponse as unknown as Response
231+
);
232+
233+
renderWithTheme(<ServiceCatalogItem {...defaultProps} />);
234+
235+
const form = screen.getByTestId("item-request-form");
236+
fireEvent.submit(form);
237+
238+
await waitFor(() => {
239+
expect(mockNotify).toHaveBeenCalledWith(
240+
expect.objectContaining({
241+
type: "error",
242+
title: "Service couldn't be submitted",
243+
message: "Give it a moment and try it again",
244+
})
245+
);
246+
});
247+
});
248+
249+
it("should handle 422 response with unexpected structure gracefully", async () => {
250+
const errorResponse = {
251+
ok: false,
252+
status: 422,
253+
json: () =>
254+
Promise.resolve({
255+
error: "SomeError",
256+
// Missing details.base - should use fallback to empty array
257+
}),
258+
};
259+
260+
mockSubmitServiceItemRequest.mockResolvedValue(
261+
errorResponse as unknown as Response
262+
);
263+
264+
renderWithTheme(<ServiceCatalogItem {...defaultProps} />);
265+
266+
const form = screen.getByTestId("item-request-form");
267+
fireEvent.submit(form);
268+
269+
// Should not throw and should handle gracefully (no notification when no errors in base array)
270+
await waitFor(() => {
271+
expect(mockNotify).not.toHaveBeenCalled();
272+
});
273+
});
274+
275+
it("should show error notification for non-422 error responses", async () => {
276+
const errorResponse = {
277+
ok: false,
278+
status: 500,
279+
};
280+
281+
mockSubmitServiceItemRequest.mockResolvedValue(
282+
errorResponse as unknown as Response
283+
);
284+
285+
renderWithTheme(<ServiceCatalogItem {...defaultProps} />);
286+
287+
const form = screen.getByTestId("item-request-form");
288+
fireEvent.submit(form);
289+
290+
await waitFor(() => {
291+
expect(mockNotify).toHaveBeenCalledWith(
292+
expect.objectContaining({
293+
type: "error",
294+
title: "Service couldn't be submitted",
295+
message: "Give it a moment and try it again",
296+
})
297+
);
298+
});
299+
});
300+
301+
it("should show error notification when response is undefined", async () => {
302+
mockSubmitServiceItemRequest.mockResolvedValue(undefined);
303+
304+
renderWithTheme(<ServiceCatalogItem {...defaultProps} />);
305+
306+
const form = screen.getByTestId("item-request-form");
307+
fireEvent.submit(form);
308+
309+
await waitFor(() => {
310+
expect(mockNotify).toHaveBeenCalledWith(
311+
expect.objectContaining({
312+
type: "error",
313+
title: "Service couldn't be submitted",
314+
message: "Give it a moment and try it again",
315+
})
316+
);
317+
});
318+
});
319+
});
320+
});

0 commit comments

Comments
 (0)