Skip to content

Commit cdcb06e

Browse files
fix: feedback from PR
Signed-off-by: Priscila Moneo <priscila_moneo@hotmail.com.ar>
1 parent 359a112 commit cdcb06e

4 files changed

Lines changed: 235 additions & 25 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import configureStore from "redux-mock-store";
5+
import thunk from "redux-thunk";
6+
import flushPromises from "flush-promises";
7+
import {
8+
postRequest,
9+
putRequest
10+
} from "openstack-uicore-foundation/lib/utils/actions";
11+
import { saveEmailTemplate } from "../email-actions";
12+
import * as methods from "../../utils/methods";
13+
14+
jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
15+
__esModule: true,
16+
...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
17+
postRequest: jest.fn(),
18+
putRequest: jest.fn()
19+
}));
20+
21+
jest.mock("../marketing-actions", () => ({
22+
saveMarketingSetting: jest.fn()
23+
}));
24+
25+
const requestMock =
26+
(requestActionCreator, receiveActionCreator) => () => (dispatch) => {
27+
if (requestActionCreator && typeof requestActionCreator === "function") {
28+
dispatch(requestActionCreator({}));
29+
}
30+
return new Promise((resolve) => {
31+
if (typeof receiveActionCreator === "function") {
32+
dispatch(receiveActionCreator({ response: { id: 1 } }));
33+
} else {
34+
dispatch(receiveActionCreator);
35+
}
36+
resolve({ response: { id: 1 } });
37+
});
38+
};
39+
40+
describe("saveEmailTemplate", () => {
41+
const middlewares = [thunk];
42+
const mockStore = configureStore(middlewares);
43+
44+
beforeEach(() => {
45+
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
46+
postRequest.mockImplementation(requestMock);
47+
putRequest.mockImplementation(requestMock);
48+
});
49+
50+
afterEach(() => {
51+
jest.restoreAllMocks();
52+
});
53+
54+
describe("create path (entity has no id)", () => {
55+
it("returns a Promise", async () => {
56+
const store = mockStore({});
57+
const result = store.dispatch(
58+
saveEmailTemplate({ identifier: "test-template" })
59+
);
60+
expect(result).toBeInstanceOf(Promise);
61+
await expect(result).resolves.toBeUndefined();
62+
});
63+
64+
it("dispatches TEMPLATE_ADDED then STOP_LOADING on success", async () => {
65+
const store = mockStore({});
66+
store.dispatch(saveEmailTemplate({ identifier: "test-template" }));
67+
await flushPromises();
68+
69+
const actionTypes = store.getActions().map((a) => a.type);
70+
expect(actionTypes).toContain("TEMPLATE_ADDED");
71+
expect(actionTypes).toContain("STOP_LOADING");
72+
expect(actionTypes.indexOf("STOP_LOADING")).toBeGreaterThan(
73+
actionTypes.indexOf("TEMPLATE_ADDED")
74+
);
75+
});
76+
});
77+
78+
describe("update path (entity has id)", () => {
79+
it("returns a Promise", async () => {
80+
const store = mockStore({});
81+
const result = store.dispatch(
82+
saveEmailTemplate({ id: 1, identifier: "test-template" })
83+
);
84+
expect(result).toBeInstanceOf(Promise);
85+
await expect(result).resolves.toBeUndefined();
86+
});
87+
88+
it("dispatches TEMPLATE_UPDATED then STOP_LOADING on success", async () => {
89+
const store = mockStore({});
90+
store.dispatch(saveEmailTemplate({ id: 1, identifier: "test-template" }));
91+
await flushPromises();
92+
93+
const actionTypes = store.getActions().map((a) => a.type);
94+
expect(actionTypes).toContain("TEMPLATE_UPDATED");
95+
expect(actionTypes).toContain("STOP_LOADING");
96+
expect(actionTypes.indexOf("STOP_LOADING")).toBeGreaterThan(
97+
actionTypes.indexOf("TEMPLATE_UPDATED")
98+
);
99+
});
100+
});
101+
});

src/actions/email-actions.js

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
createAction,
2222
stopLoading,
2323
startLoading,
24-
showMessage,
2524
showSuccessMessage,
2625
authErrorHandler,
2726
fetchResponseHandler,
@@ -30,7 +29,6 @@ import {
3029
} from "openstack-uicore-foundation/lib/utils/actions";
3130
import URI from "urijs";
3231
import debounce from "lodash/debounce";
33-
import history from "../history";
3432
import { checkOrFilter, getAccessTokenSafely } from "../utils/methods";
3533
import { saveMarketingSetting } from "./marketing-actions";
3634
import {
@@ -137,40 +135,37 @@ export const saveEmailTemplate =
137135
const params = { access_token: accessToken, expand: "parent,versions" };
138136

139137
if (entity.id) {
140-
putRequest(
138+
return putRequest(
141139
null,
142140
createAction(TEMPLATE_UPDATED),
143141
`${window.EMAIL_API_BASE_URL}/api/v1/mail-templates/${entity.id}`,
144142
normalizedEntity,
145143
customErrorHandler,
146144
entity
147-
)(params)(dispatch).then(() => {
148-
if (!noAlert)
149-
dispatch(showSuccessMessage(T.translate("emails.template_saved")));
150-
else dispatch(stopLoading());
151-
});
152-
} else {
153-
const success_message = {
154-
title: T.translate("general.done"),
155-
html: T.translate("emails.template_created"),
156-
type: "success"
157-
};
158-
159-
postRequest(
145+
)(params)(dispatch)
146+
.then(() => {
147+
if (!noAlert)
148+
dispatch(showSuccessMessage(T.translate("emails.template_saved")));
149+
})
150+
.finally(() => {
151+
dispatch(stopLoading());
152+
});
153+
}
154+
return postRequest(
160155
null,
161156
createAction(TEMPLATE_ADDED),
162157
`${window.EMAIL_API_BASE_URL}/api/v1/mail-templates`,
163158
normalizedEntity,
164159
customErrorHandler,
165160
entity
166-
)(params)(dispatch).then((payload) => {
167-
dispatch(
168-
showMessage(success_message, () => {
169-
history.push(`/app/emails/templates/${payload.response.id}`);
170-
})
171-
);
172-
});
173-
}
161+
)(params)(dispatch)
162+
.then(() => {
163+
dispatch(showSuccessMessage(T.translate("emails.template_created")));
164+
})
165+
.finally(() => {
166+
dispatch(stopLoading());
167+
});
168+
174169
};
175170

176171
export const deleteEmailTemplate = (templateId) => async (dispatch) => {
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from "react";
2+
import { act, screen } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import "@testing-library/jest-dom";
5+
import flushPromises from "flush-promises";
6+
import { renderWithRedux } from "../../../utils/test-utils";
7+
import EmailTemplateListPage from "../email-template-list-page";
8+
import {
9+
getEmailTemplates,
10+
deleteEmailTemplate
11+
} from "../../../actions/email-actions";
12+
13+
jest.mock("../../../actions/email-actions", () => ({
14+
getEmailTemplates: jest.fn(),
15+
deleteEmailTemplate: jest.fn()
16+
}));
17+
18+
jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
19+
__esModule: true,
20+
default: ({ onEdit, onDelete }) => (
21+
<div>
22+
<button
23+
type="button"
24+
onClick={() => onEdit({ id: 1, identifier: "test-template" })}
25+
>
26+
edit-row
27+
</button>
28+
<button
29+
type="button"
30+
onClick={() => onDelete({ id: 1, identifier: "test-template" })}
31+
>
32+
delete-row
33+
</button>
34+
</div>
35+
)
36+
}));
37+
38+
jest.mock(
39+
"openstack-uicore-foundation/lib/components/mui/search-input",
40+
() => ({
41+
__esModule: true,
42+
default: () => <input placeholder="search-templates" />
43+
})
44+
);
45+
46+
jest.mock("i18n-react/dist/i18n-react", () => ({
47+
__esModule: true,
48+
default: { translate: (key) => key }
49+
}));
50+
51+
const mockHistory = { push: jest.fn() };
52+
53+
const initialState = {
54+
emailTemplateListState: {
55+
templates: [
56+
{
57+
id: 1,
58+
identifier: "test-template",
59+
subject: "Test Subject",
60+
from_email: "test@example.com"
61+
}
62+
],
63+
totalTemplates: 1,
64+
perPage: 10,
65+
currentPage: 1,
66+
term: "",
67+
order: "id",
68+
orderDir: 1
69+
}
70+
};
71+
72+
describe("EmailTemplateListPage", () => {
73+
beforeEach(() => {
74+
jest.clearAllMocks();
75+
getEmailTemplates.mockReturnValue(() => Promise.resolve());
76+
deleteEmailTemplate.mockReturnValue(() => Promise.resolve());
77+
});
78+
79+
it("reloads the list after a successful delete", async () => {
80+
renderWithRedux(<EmailTemplateListPage history={mockHistory} />, {
81+
initialState
82+
});
83+
84+
await act(async () => {
85+
await userEvent.click(screen.getByRole("button", { name: "delete-row" }));
86+
await flushPromises();
87+
});
88+
89+
// Call 1: useEffect on mount; call 2: handleDeleteEmailTemplate .finally()
90+
expect(getEmailTemplates).toHaveBeenCalledTimes(2);
91+
});
92+
93+
it("re-syncs the list after a failed delete", async () => {
94+
deleteEmailTemplate.mockReturnValue(() =>
95+
Promise.reject(new Error("delete failed"))
96+
);
97+
98+
renderWithRedux(<EmailTemplateListPage history={mockHistory} />, {
99+
initialState
100+
});
101+
102+
await act(async () => {
103+
await userEvent.click(screen.getByRole("button", { name: "delete-row" }));
104+
await flushPromises();
105+
});
106+
107+
// Call 1: useEffect on mount; call 2: handleDeleteEmailTemplate .finally() fires even on rejection
108+
expect(getEmailTemplates).toHaveBeenCalledTimes(2);
109+
});
110+
});

src/pages/emails/email-template-list-page.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ const EmailTemplateListPage = ({
7979
};
8080

8181
const handleDeleteEmailTemplate = (row) => {
82-
removeEmailTemplate(row.id);
82+
removeEmailTemplate(row.id)
83+
.finally(() =>
84+
fetchEmailTemplates(term, currentPage, perPage, order, orderDir)
85+
)
86+
.catch(() => {});
8387
};
8488

8589
const columns = [

0 commit comments

Comments
 (0)