Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2e53b4e
fix: migrate edit email template into mui popup
tomrndom Aug 31, 2026
50bad83
fix: adjust test cases on email template list page
tomrndom Aug 31, 2026
d820d57
fix: apply adjustments on template and add guards
tomrndom Aug 31, 2026
bb4e3e0
fix: restore routes and edit page, extract json sample data popup, ad…
tomrndom Aug 31, 2026
2fd8375
fix: adjust route param, sync entityReady with template_id
tomrndom Aug 31, 2026
ac62343
fix: adjust test cases, fix issue with input template re search
tomrndom Aug 31, 2026
2552e63
fix: fix bug on screen resize
tomrndom Sep 1, 2026
92abbf0
fix: restore useState from rebase
tomrndom Sep 2, 2026
02c79d0
fix: restore edit email template page pattern and remove popup
tomrndom Sep 8, 2026
fef3fee
fix: rollback actions and redirect changes
tomrndom Sep 8, 2026
1002a6f
fix: add catch, fix issue on template mode changes
tomrndom Sep 8, 2026
5b8ab28
fix: simplify prop names, extract mjml toggle from template
tomrndom Sep 18, 2026
9470026
fix: update tests cases, update email input to allow previous props, …
tomrndom Sep 18, 2026
bada02d
fix: adjust styles on width, extract code to simplify template, renam…
tomrndom Sep 18, 2026
ba942ea
fix: adjust plainValue assign on email input, redirect if fail to fet…
tomrndom Sep 23, 2026
00a449f
fix: rollback email-template-input, use updated master version
tomrndom Sep 23, 2026
1eedb4c
fix: add sequenced guard on email templates, use local confirmDialog,…
tomrndom Sep 24, 2026
22028eb
fix: restore mui migration of email-template-input, add unit tests
tomrndom Sep 24, 2026
2ebc6f8
fix: adjust styles, replace hardcoded text with i18n strings, separat…
tomrndom Sep 24, 2026
b83bf01
fix: adjust tab change target
tomrndom Sep 24, 2026
c9090e8
fix: fix: move forms to formik, use snackbar errors, simplify templa…
tomrndom Oct 1, 2026
266be98
fix: unify sequenced into methods, move redirect to template from action
tomrndom Oct 2, 2026
8a3cbf6
fix: fix input and form comments, add regression tests
tomrndom Oct 28, 2026
908f443
fix: adjust actions, cancel queued render template
tomrndom Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 104 additions & 45 deletions src/actions/__tests__/email-actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,34 @@ import {
} from "openstack-uicore-foundation/lib/utils/actions";
import {
saveEmailTemplate,
getEmailTemplates,
getEmailTemplate,
resetTemplateForm,
deleteEmailTemplate,
queryTemplates,
buildRenderPayload,
normalizeRenderErrors
} from "../email-actions";
import * as methods from "../../utils/methods";

jest.mock("../../history", () => ({ push: jest.fn() }));

jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
__esModule: true,
...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
postRequest: jest.fn(),
putRequest: jest.fn(),
getRequest: jest.fn(),
deleteRequest: jest.fn()
deleteRequest: jest.fn(),
fetchErrorHandler: jest.fn()
}));

jest.mock("../marketing-actions", () => ({
saveMarketingSetting: jest.fn()
}));

jest.mock("../../history", () => ({
__esModule: true,
default: { push: jest.fn() }
}));

const requestMock =
(requestActionCreator, receiveActionCreator) => () => (dispatch) => {
if (requestActionCreator && typeof requestActionCreator === "function") {
Expand All @@ -49,23 +55,6 @@ const requestMock =
});
};

const getRequestMock =
(requestActionCreator, receiveActionCreator, _url, _handler, syncPayload) =>
() =>
(dispatch) => {
if (requestActionCreator && typeof requestActionCreator === "function") {
dispatch(requestActionCreator(syncPayload || {}));
}
return new Promise((resolve) => {
if (typeof receiveActionCreator === "function") {
dispatch(receiveActionCreator({ response: {} }));
} else {
dispatch(receiveActionCreator);
}
resolve({});
});
};

const deleteRequestMock =
(_requestActionCreator, receiveAction) => () => (dispatch) =>
new Promise((resolve) => {
Expand All @@ -92,15 +81,6 @@ describe("saveEmailTemplate", () => {
});

describe("create path (entity has no id)", () => {
it("returns a Promise", async () => {
const store = mockStore({});
const result = store.dispatch(
saveEmailTemplate({ identifier: "test-template" })
);
expect(result).toBeInstanceOf(Promise);
await expect(result).resolves.toBeUndefined();
});

it("dispatches TEMPLATE_ADDED then STOP_LOADING on success", async () => {
const store = mockStore({});
store.dispatch(saveEmailTemplate({ identifier: "test-template" }));
Expand All @@ -116,15 +96,6 @@ describe("saveEmailTemplate", () => {
});

describe("update path (entity has id)", () => {
it("returns a Promise", async () => {
const store = mockStore({});
const result = store.dispatch(
saveEmailTemplate({ id: 1, identifier: "test-template" })
);
expect(result).toBeInstanceOf(Promise);
await expect(result).resolves.toBeUndefined();
});

it("dispatches TEMPLATE_UPDATED then STOP_LOADING on success", async () => {
const store = mockStore({});
store.dispatch(saveEmailTemplate({ id: 1, identifier: "test-template" }));
Expand All @@ -140,26 +111,88 @@ describe("saveEmailTemplate", () => {
});
});

describe("getEmailTemplates", () => {
describe("getEmailTemplate - stale response guard", () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);

const isTemplateFetchUrl = (url) => /\/mail-templates\/[^/]+$/.test(url);

// each template fetch stays in flight until the test calls resolvers[id]()
const mockDeferredTemplateFetches = () => {
const resolvers = {};
getRequest.mockImplementation(
(requestActionCreator, receiveActionCreator, url) => () => (dispatch) => {
if (isTemplateFetchUrl(url)) {
const id = Number(url.split("/").pop());
return new Promise((resolve) => {
resolvers[id] = () => {
dispatch(receiveActionCreator({ response: { id } }));
resolve();
};
});
}
if (requestActionCreator) dispatch(requestActionCreator({}));
dispatch(receiveActionCreator({ response: {} }));
return Promise.resolve();
}
);
return resolvers;
};

beforeEach(() => {
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
getRequest.mockImplementation(getRequestMock);
});

afterEach(() => {
jest.restoreAllMocks();
});

it("dispatches REQUEST_TEMPLATES with page and perPage", async () => {
it("drops an older template's response after a newer template's response already landed", async () => {
const resolvers = mockDeferredTemplateFetches();

const store = mockStore({});
store.dispatch(getEmailTemplates("foo", 2, 25));

// User opens template 5, then quickly navigates to template 8 before
// template 5's fetch settles - both requests are genuinely in flight when
// template 8's response lands first.
store.dispatch(getEmailTemplate("5"));
await flushPromises();
store.dispatch(getEmailTemplate("8"));
await flushPromises();
resolvers[8]();
await flushPromises();
resolvers[5]();
await flushPromises();

const receivedIds = store
.getActions()
.filter((a) => a.type === "RECEIVE_TEMPLATE")
.map((a) => a.payload.response.id);

expect(receivedIds).toEqual([8]);
});

it("drops a template's response that lands after resetTemplateForm supersedes it", async () => {
const resolvers = mockDeferredTemplateFetches();

const store = mockStore({});

// User opens template 5, then navigates back to the list and clicks
// "Add new" before template 5's fetch settles.
store.dispatch(getEmailTemplate("5"));
await flushPromises();
store.dispatch(resetTemplateForm());
await flushPromises();
resolvers[5]();
await flushPromises();

const actionTypes = store.getActions().map((a) => a.type);

const req = store.getActions().find((a) => a.type === "REQUEST_TEMPLATES");
expect(req.payload).toMatchObject({ page: 2, perPage: 25 });
expect(actionTypes).not.toContain("RECEIVE_TEMPLATE");
expect(actionTypes).toContain("RESET_TEMPLATE_FORM");
expect(actionTypes.lastIndexOf("STOP_LOADING")).toBeGreaterThan(
actionTypes.indexOf("START_LOADING")
);
});
});

Expand Down Expand Up @@ -189,6 +222,32 @@ describe("deleteEmailTemplate", () => {
});
});

describe("queryTemplates", () => {
beforeEach(() => {
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
global.fetch = jest.fn(() => Promise.reject(new Error("network down")));
});

afterEach(() => {
delete global.fetch;
jest.useRealTimers();
jest.restoreAllMocks();
});

it("settles the callback with no options when the search request fails", async () => {
jest.useFakeTimers();
const callback = jest.fn();

queryTemplates("welcome", callback);
jest.runAllTimers();
// flushPromises schedules on a timer, so it needs the real ones
jest.useRealTimers();
await flushPromises();

expect(callback).toHaveBeenCalledWith([]);
});
});

describe("buildRenderPayload", () => {
it("sends the mjml param when isMjml is true", () => {
expect(
Expand Down
Loading
Loading