From 5a908680858ad728a5de8c281df8f03a110f50f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 5 Aug 2026 16:52:39 -0300 Subject: [PATCH 1/3] fix: add delete on sponsor form managed table, new actions, constants and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/actions/sponsor-forms-actions.js | 35 ++- .../__tests__/sponsor-forms-tab.test.js | 267 ++++++++++++++++++ .../tabs/sponsor-forms-tab/index.js | 39 ++- .../sponsor-page-forms-list-reducer.js | 19 +- src/utils/constants.js | 5 + 5 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js index 5492628e7..2216019bf 100644 --- a/src/actions/sponsor-forms-actions.js +++ b/src/actions/sponsor-forms-actions.js @@ -54,6 +54,7 @@ export const RECEIVE_SPONSOR_MANAGED_FORMS = "RECEIVE_SPONSOR_MANAGED_FORMS"; export const REQUEST_SPONSOR_MANAGED_FORM = "REQUEST_SPONSOR_MANAGED_FORM"; export const RECEIVE_SPONSOR_MANAGED_FORM = "RECEIVE_SPONSOR_MANAGED_FORM"; export const SPONSOR_MANAGED_FORMS_ADDED = "SPONSOR_MANAGED_FORMS_ADDED"; +export const SPONSOR_MANAGED_FORM_DELETED = "SPONSOR_MANAGED_FORM_DELETED"; export const SPONSOR_MANAGED_FORMS_UPGRADED = "SPONSOR_MANAGED_FORMS_UPGRADED"; export const SPONSOR_MANAGED_FORMS_OVERRIDEN = "SPONSOR_MANAGED_FORMS_OVERRIDEN"; @@ -564,7 +565,7 @@ export const getSponsorManagedForms = const params = { page, fields: - "id,code,name,is_archived,opens_at,expires_at,items_count,allowed_add_ons", + "id,code,name,is_archived,opens_at,expires_at,items_count,allowed_add_ons,assignment_type", expands: "allowed_add_ons", per_page: perPage, access_token: accessToken @@ -689,6 +690,38 @@ export const upgradeSponsorManagedForm = }); }; +export const deleteSponsorManagedForm = + (formId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + const params = { access_token: accessToken }; + + dispatch(startLoading()); + + return deleteRequest( + null, + createAction(SPONSOR_MANAGED_FORM_DELETED)({ formId }), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/managed-forms/${formId}`, + null, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("sponsor_forms.form_delete_success") + }) + ); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; + const normalizeSponsorManagedForm = (entity) => { const normalizedEntity = { show_form_ids: entity.forms, diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js new file mode 100644 index 000000000..64a68b04e --- /dev/null +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js @@ -0,0 +1,267 @@ +import React from "react"; +import userEvent from "@testing-library/user-event"; +import { act, screen, waitFor } from "@testing-library/react"; +import { GlobalConfirmDialog } from "openstack-uicore-foundation/lib/components/mui/show-confirm-dialog"; +import SponsorFormsTab from "../index"; +import { renderWithRedux } from "../../../../../../utils/test-utils"; +import { DEFAULT_STATE as sponsorFormsDefaultState } from "../../../../../../reducers/sponsors/sponsor-page-forms-list-reducer"; +import { + getSponsorManagedForms, + getSponsorCustomizedForms, + deleteSponsorManagedForm, + deleteSponsorCustomizedForm +} from "../../../../../../actions/sponsor-forms-actions"; + +// Mocks + +jest.mock( + "../components/add-sponsor-form-template-popup", + () => + function MockAddSponsorFormTemplatePopup({ onClose, onSubmit }) { + return ( +
+ + +
+ ); + } +); + +jest.mock( + "../components/customized-form/customized-form-popup", + () => + function MockCustomizedFormPopup({ onClose }) { + return ( +
+ +
+ ); + } +); + +jest.mock("../../../../../../actions/sponsor-forms-actions", () => ({ + ...jest.requireActual("../../../../../../actions/sponsor-forms-actions"), + getSponsorManagedForms: jest.fn(() => () => Promise.resolve()), + getSponsorCustomizedForms: jest.fn(() => () => Promise.resolve()), + saveSponsorManagedForm: jest.fn(() => () => Promise.resolve()), + overrideSponsorManagedForm: jest.fn(() => () => Promise.resolve()), + archiveSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()), + unarchiveSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()), + deleteSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()), + deleteSponsorManagedForm: jest.fn(() => () => Promise.resolve()) +})); + +// Helpers + +const createSponsor = (overrides = {}) => ({ + id: 1, + ...overrides +}); + +const createManagedForm = (id, overrides = {}) => ({ + id, + code: `MANAGED-${id}`, + name: `Managed Form ${id}`, + items_count: 0, + allowed_add_ons: [], + assignment_type: "Explicit", + ...overrides +}); + +const createCustomizedForm = (id, overrides = {}) => ({ + id, + code: `CODE-${id}`, + name: `Form ${id}`, + items_count: 0, + allowed_add_ons: [], + is_archived: false, + ...overrides +}); + +const defaultState = { + sponsorPageFormsListState: { + ...sponsorFormsDefaultState, + managedForms: { + ...sponsorFormsDefaultState.managedForms, + forms: [], + totalCount: 0 + }, + customizedForms: { + ...sponsorFormsDefaultState.customizedForms, + forms: [], + totalCount: 0 + }, + showArchived: false, + term: "" + }, + currentSummitState: { + currentSummit: { + id: 1, + time_zone: { name: "UTC" } + } + }, + currentSponsorState: { + entity: { id: 1 }, + errors: {} + } +}; + +const renderWithConfirmDialog = (ui, options) => + renderWithRedux( + <> + + {ui} + , + options + ); + +describe("SponsorFormsTab", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("managed forms delete", () => { + it("shows the delete action only for EXPLICIT managed forms", () => { + renderWithConfirmDialog(, { + initialState: { + ...defaultState, + sponsorPageFormsListState: { + ...defaultState.sponsorPageFormsListState, + managedForms: { + ...defaultState.sponsorPageFormsListState.managedForms, + forms: [ + createManagedForm(1, { assignment_type: "Explicit" }), + createManagedForm(2, { assignment_type: "Implicit" }) + ], + totalCount: 2 + } + } + } + }); + + expect(screen.getAllByTestId("DeleteIcon")).toHaveLength(1); + }); + + it("calls deleteSponsorManagedForm and refreshes both lists when delete is confirmed", async () => { + renderWithConfirmDialog(, { + initialState: { + ...defaultState, + sponsorPageFormsListState: { + ...defaultState.sponsorPageFormsListState, + managedForms: { + ...defaultState.sponsorPageFormsListState.managedForms, + forms: [createManagedForm(1, { assignment_type: "Explicit" })], + totalCount: 1 + } + } + } + }); + + const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + await act(async () => { + await userEvent.click(deleteButton); + }); + + expect( + await screen.findByText("general.are_you_sure") + ).toBeInTheDocument(); + + await act(async () => { + await userEvent.click( + await screen.findByRole("button", { + name: /general\.yes_delete|confirm/i + }) + ); + }); + + await waitFor(() => { + expect(deleteSponsorManagedForm).toHaveBeenCalledWith(1); + }); + + await waitFor(() => { + expect(getSponsorManagedForms).toHaveBeenCalledTimes(2); // mount + after delete + expect(getSponsorCustomizedForms).toHaveBeenCalledTimes(2); + }); + }); + + it("does not call deleteSponsorManagedForm when delete is cancelled", async () => { + renderWithConfirmDialog(, { + initialState: { + ...defaultState, + sponsorPageFormsListState: { + ...defaultState.sponsorPageFormsListState, + managedForms: { + ...defaultState.sponsorPageFormsListState.managedForms, + forms: [createManagedForm(1, { assignment_type: "Explicit" })], + totalCount: 1 + } + } + } + }); + + const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + await act(async () => { + await userEvent.click(deleteButton); + }); + + expect( + await screen.findByText("general.are_you_sure") + ).toBeInTheDocument(); + + await act(async () => { + await userEvent.click( + await screen.findByRole("button", { name: /cancel|general\.cancel/i }) + ); + }); + + expect(deleteSponsorManagedForm).not.toHaveBeenCalled(); + expect(getSponsorManagedForms).toHaveBeenCalledTimes(1); + }); + }); + + describe("customized forms delete", () => { + it("calls deleteSponsorCustomizedForm and refreshes both lists when delete is confirmed", async () => { + renderWithConfirmDialog(, { + initialState: { + ...defaultState, + sponsorPageFormsListState: { + ...defaultState.sponsorPageFormsListState, + customizedForms: { + ...defaultState.sponsorPageFormsListState.customizedForms, + forms: [createCustomizedForm(1)], + totalCount: 1 + } + } + } + }); + + const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + await act(async () => { + await userEvent.click(deleteButton); + }); + + expect( + await screen.findByText("general.are_you_sure") + ).toBeInTheDocument(); + + await act(async () => { + await userEvent.click( + await screen.findByRole("button", { + name: /general\.yes_delete|confirm/i + }) + ); + }); + + await waitFor(() => { + expect(deleteSponsorCustomizedForm).toHaveBeenCalledWith(1); + }); + + await waitFor(() => { + expect(getSponsorCustomizedForms).toHaveBeenCalledTimes(2); // mount + after delete + expect(getSponsorManagedForms).toHaveBeenCalledTimes(2); + }); + }); + }); +}); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js index 5e499a935..8127cb2bd 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js @@ -34,12 +34,16 @@ import { getSponsorManagedForms, saveSponsorManagedForm, overrideSponsorManagedForm, - unarchiveSponsorCustomizedForm + unarchiveSponsorCustomizedForm, + deleteSponsorManagedForm } from "../../../../../actions/sponsor-forms-actions"; import CustomAlert from "../../../../../components/mui/custom-alert"; import AddSponsorFormTemplatePopup from "./components/add-sponsor-form-template-popup"; import CustomizedFormPopup from "./components/customized-form/customized-form-popup"; -import { DEFAULT_CURRENT_PAGE } from "../../../../../utils/constants"; +import { + DEFAULT_CURRENT_PAGE, + MANAGED_FORM_ASSIGNMENT_TYPE +} from "../../../../../utils/constants"; import showConfirmDialog from "../../../../../components/mui/showConfirmDialog"; const SponsorFormsTab = ({ @@ -56,7 +60,8 @@ const SponsorFormsTab = ({ overrideSponsorManagedForm, archiveSponsorCustomizedForm, unarchiveSponsorCustomizedForm, - deleteSponsorCustomizedForm + deleteSponsorCustomizedForm, + deleteSponsorManagedForm }) => { const [openPopup, setOpenPopup] = useState(null); const [customFormEdit, setCustomFormEdit] = useState(null); @@ -175,6 +180,27 @@ const SponsorFormsTab = ({ setCustomFormEdit(item); }; + const handleManagedDelete = (itemId) => { + deleteSponsorManagedForm(itemId).then(() => { + getSponsorCustomizedForms( + term, + DEFAULT_CURRENT_PAGE, + customizedForms.perPage, + customizedForms.order, + customizedForms.orderDir, + showArchived + ); + getSponsorManagedForms( + term, + DEFAULT_CURRENT_PAGE, + managedForms.perPage, + managedForms.order, + managedForms.orderDir, + showArchived + ); + }); + }; + const handleCustomizedDelete = (itemId) => { deleteSponsorCustomizedForm(itemId).then(() => { getSponsorCustomizedForms( @@ -506,6 +532,10 @@ const SponsorFormsTab = ({ onPageChange={handleManagedPageChange} onPerPageChange={handleManagedPerPageChange} onSort={handleManagedSort} + onDelete={handleManagedDelete} + canDelete={(row) => + row.assignment_type === MANAGED_FORM_ASSIGNMENT_TYPE.EXPLICIT + } /> @@ -549,5 +579,6 @@ export default connect(mapStateToProps, { getSponsorCustomizedForms, archiveSponsorCustomizedForm, unarchiveSponsorCustomizedForm, - deleteSponsorCustomizedForm + deleteSponsorCustomizedForm, + deleteSponsorManagedForm })(SponsorFormsTab); diff --git a/src/reducers/sponsors/sponsor-page-forms-list-reducer.js b/src/reducers/sponsors/sponsor-page-forms-list-reducer.js index da056b21a..c7c4277d4 100644 --- a/src/reducers/sponsors/sponsor-page-forms-list-reducer.js +++ b/src/reducers/sponsors/sponsor-page-forms-list-reducer.js @@ -21,7 +21,8 @@ import { SPONSOR_CUSTOMIZED_FORM_ADDED, SPONSOR_CUSTOMIZED_FORM_DELETED, SPONSOR_CUSTOMIZED_FORM_ARCHIVED_CHANGED, - SPONSOR_CUSTOMIZED_FORM_UPDATED + SPONSOR_CUSTOMIZED_FORM_UPDATED, + SPONSOR_MANAGED_FORM_DELETED } from "../../actions/sponsor-forms-actions"; import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions"; import { getSafePageAfterRemove } from "../../utils/methods"; @@ -122,6 +123,7 @@ const sponsorPageFormsListReducer = (state = DEFAULT_STATE, action) => { items_count: a.items_count, allowed_add_ons: a.allowed_add_ons, is_archived: a.is_archived, + assignment_type: a.assignment_type, opens_at: opensAt, expires_at: expiresAt }; @@ -277,6 +279,21 @@ const sponsorPageFormsListReducer = (state = DEFAULT_STATE, action) => { } }; } + case SPONSOR_MANAGED_FORM_DELETED: { + const { formId } = payload; + const forms = state.managedForms.forms.filter( + (form) => form.id !== formId + ); + + return { + ...state, + managedForms: { + ...state.managedForms, + forms, + totalCount: state.managedForms.totalCount - 1 + } + }; + } default: return state; } diff --git a/src/utils/constants.js b/src/utils/constants.js index 846f53c8e..b720afcb8 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -332,6 +332,11 @@ export const SPONSOR_MANAGED_PAGE_ASSIGNMENT = { IMPLICIT: "Implicit" }; +export const MANAGED_FORM_ASSIGNMENT_TYPE = { + EXPLICIT: "Explicit", + IMPLICIT: "Implicit" +}; + export const ACCESS_ROUTES = { ADMIN_SPONSORS: "admin-sponsors", SPONSORS: "sponsors" From f1382fa07500446a3cb0448f71751e256a0748ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 5 Aug 2026 17:53:24 -0300 Subject: [PATCH 2/3] fix: add catch on delete function, adjust tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../__tests__/sponsor-forms-tab.test.js | 133 ++++++++++++++---- .../tabs/sponsor-forms-tab/index.js | 38 ++--- 2 files changed, 129 insertions(+), 42 deletions(-) diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js index 64a68b04e..f51df8e00 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js @@ -1,14 +1,20 @@ import React from "react"; +import { createStore, combineReducers, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; import userEvent from "@testing-library/user-event"; -import { act, screen, waitFor } from "@testing-library/react"; +import { act, screen, waitFor, within } from "@testing-library/react"; import { GlobalConfirmDialog } from "openstack-uicore-foundation/lib/components/mui/show-confirm-dialog"; +import { deleteRequest } from "openstack-uicore-foundation/lib/utils/actions"; import SponsorFormsTab from "../index"; import { renderWithRedux } from "../../../../../../utils/test-utils"; -import { DEFAULT_STATE as sponsorFormsDefaultState } from "../../../../../../reducers/sponsors/sponsor-page-forms-list-reducer"; +import * as methods from "../../../../../../utils/methods"; +import sponsorPageFormsListReducer, { + DEFAULT_STATE as sponsorFormsDefaultState +} from "../../../../../../reducers/sponsors/sponsor-page-forms-list-reducer"; import { + RECEIVE_SPONSOR_MANAGED_FORMS, getSponsorManagedForms, getSponsorCustomizedForms, - deleteSponsorManagedForm, deleteSponsorCustomizedForm } from "../../../../../../actions/sponsor-forms-actions"; @@ -41,6 +47,10 @@ jest.mock( } ); +// deleteSponsorManagedForm is intentionally left un-mocked (falls through to +// jest.requireActual below): the delete-confirm test exercises the real thunk +// against a real store + reducer, with only the uicore HTTP layer mocked, so +// the SPONSOR_MANAGED_FORM_DELETED reducer case actually runs. jest.mock("../../../../../../actions/sponsor-forms-actions", () => ({ ...jest.requireActual("../../../../../../actions/sponsor-forms-actions"), getSponsorManagedForms: jest.fn(() => () => Promise.resolve()), @@ -49,8 +59,13 @@ jest.mock("../../../../../../actions/sponsor-forms-actions", () => ({ overrideSponsorManagedForm: jest.fn(() => () => Promise.resolve()), archiveSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()), unarchiveSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()), - deleteSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()), - deleteSponsorManagedForm: jest.fn(() => () => Promise.resolve()) + deleteSponsorCustomizedForm: jest.fn(() => () => Promise.resolve()) +})); + +jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({ + __esModule: true, + ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"), + deleteRequest: jest.fn() })); // Helpers @@ -117,9 +132,48 @@ const renderWithConfirmDialog = (ui, options) => options ); +// Passthrough reducer for state slices the delete flow doesn't mutate. +const staticReducer = + (initialState) => + (state = initialState) => + state; + +// Real store + real reducer, seeded via the actual RECEIVE_SPONSOR_MANAGED_FORMS +// action, so the SPONSOR_MANAGED_FORM_DELETED case genuinely runs and the table +// re-renders from real state instead of a static mock. +const createRealStore = (managedFormsData) => { + const store = createStore( + combineReducers({ + sponsorPageFormsListState: sponsorPageFormsListReducer, + currentSummitState: staticReducer(defaultState.currentSummitState), + currentSponsorState: staticReducer(defaultState.currentSponsorState) + }), + applyMiddleware(thunk) + ); + + store.dispatch({ + type: RECEIVE_SPONSOR_MANAGED_FORMS, + payload: { + response: { + current_page: 1, + last_page: 1, + total: managedFormsData.length, + data: managedFormsData + } + } + }); + + return store; +}; + describe("SponsorFormsTab", () => { beforeEach(() => { jest.clearAllMocks(); + jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN"); + }); + + afterEach(() => { + jest.restoreAllMocks(); }); describe("managed forms delete", () => { @@ -141,27 +195,43 @@ describe("SponsorFormsTab", () => { } }); - expect(screen.getAllByTestId("DeleteIcon")).toHaveLength(1); + const explicitRow = screen.getByText("Managed Form 1").closest("tr"); + const implicitRow = screen.getByText("Managed Form 2").closest("tr"); + + expect(within(explicitRow).getByTestId("DeleteIcon")).toBeInTheDocument(); + expect( + within(implicitRow).queryByTestId("DeleteIcon") + ).not.toBeInTheDocument(); }); - it("calls deleteSponsorManagedForm and refreshes both lists when delete is confirmed", async () => { - renderWithConfirmDialog(, { - initialState: { - ...defaultState, - sponsorPageFormsListState: { - ...defaultState.sponsorPageFormsListState, - managedForms: { - ...defaultState.sponsorPageFormsListState.managedForms, - forms: [createManagedForm(1, { assignment_type: "Explicit" })], - totalCount: 1 - } - } + it("calls the delete request with the correct form id and removes the row from the table when confirmed", async () => { + deleteRequest.mockImplementation( + (requestActionCreator, receiveActionCreator) => () => (dispatch) => { + dispatch( + typeof receiveActionCreator === "function" + ? receiveActionCreator({ response: {} }) + : receiveActionCreator + ); + return Promise.resolve({ response: {} }); } + ); + + const store = createRealStore([ + createManagedForm(1, { assignment_type: "Explicit" }), + createManagedForm(2, { assignment_type: "Explicit" }) + ]); + + renderWithConfirmDialog(, { + store }); - const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + expect(screen.getByText("Managed Form 1")).toBeInTheDocument(); + expect(screen.getByText("Managed Form 2")).toBeInTheDocument(); + + const deleteButtons = screen.getAllByTestId("DeleteIcon"); + const secondDeleteButton = deleteButtons[1].closest("button"); await act(async () => { - await userEvent.click(deleteButton); + await userEvent.click(secondDeleteButton); }); expect( @@ -176,9 +246,23 @@ describe("SponsorFormsTab", () => { ); }); + // Verifies the real thunk was invoked with the id of the row that was + // actually clicked (form 2), all the way down to the HTTP boundary. + await waitFor(() => { + expect(deleteRequest).toHaveBeenCalledWith( + null, + expect.objectContaining({ payload: { formId: 2 } }), + expect.stringContaining("/managed-forms/2"), + null, + expect.any(Function) + ); + }); + + // Verifies the real reducer removed the row: form 2 is gone, form 1 stays. await waitFor(() => { - expect(deleteSponsorManagedForm).toHaveBeenCalledWith(1); + expect(screen.queryByText("Managed Form 2")).not.toBeInTheDocument(); }); + expect(screen.getByText("Managed Form 1")).toBeInTheDocument(); await waitFor(() => { expect(getSponsorManagedForms).toHaveBeenCalledTimes(2); // mount + after delete @@ -186,7 +270,7 @@ describe("SponsorFormsTab", () => { }); }); - it("does not call deleteSponsorManagedForm when delete is cancelled", async () => { + it("does not call the delete request and keeps the row when delete is cancelled", async () => { renderWithConfirmDialog(, { initialState: { ...defaultState, @@ -216,8 +300,9 @@ describe("SponsorFormsTab", () => { ); }); - expect(deleteSponsorManagedForm).not.toHaveBeenCalled(); - expect(getSponsorManagedForms).toHaveBeenCalledTimes(1); + expect(deleteRequest).not.toHaveBeenCalled(); + expect(getSponsorManagedForms).toHaveBeenCalledTimes(1); // mount only + expect(screen.getByText("Managed Form 1")).toBeInTheDocument(); }); }); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js index 8127cb2bd..ee31d1565 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/index.js @@ -181,24 +181,26 @@ const SponsorFormsTab = ({ }; const handleManagedDelete = (itemId) => { - deleteSponsorManagedForm(itemId).then(() => { - getSponsorCustomizedForms( - term, - DEFAULT_CURRENT_PAGE, - customizedForms.perPage, - customizedForms.order, - customizedForms.orderDir, - showArchived - ); - getSponsorManagedForms( - term, - DEFAULT_CURRENT_PAGE, - managedForms.perPage, - managedForms.order, - managedForms.orderDir, - showArchived - ); - }); + deleteSponsorManagedForm(itemId) + .then(() => { + getSponsorCustomizedForms( + term, + DEFAULT_CURRENT_PAGE, + customizedForms.perPage, + customizedForms.order, + customizedForms.orderDir, + showArchived + ); + getSponsorManagedForms( + term, + DEFAULT_CURRENT_PAGE, + managedForms.perPage, + managedForms.order, + managedForms.orderDir, + showArchived + ); + }) + .catch(() => {}); }; const handleCustomizedDelete = (itemId) => { From 751dad7675fb0f24129fc11e139d8aae6635e24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 5 Aug 2026 18:40:48 -0300 Subject: [PATCH 3/3] fix: add test for catching error on delete request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../__tests__/sponsor-forms-tab.test.js | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js index f51df8e00..3b35cf89b 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/__tests__/sponsor-forms-tab.test.js @@ -174,6 +174,10 @@ describe("SponsorFormsTab", () => { afterEach(() => { jest.restoreAllMocks(); + // clearAllMocks()/restoreAllMocks() do not reset a plain jest.fn()'s + // mockImplementation, so a test that sets one on `deleteRequest` would + // otherwise leak it into whichever test runs next in this file. + deleteRequest.mockReset(); }); describe("managed forms delete", () => { @@ -304,6 +308,51 @@ describe("SponsorFormsTab", () => { expect(getSponsorManagedForms).toHaveBeenCalledTimes(1); // mount only expect(screen.getByText("Managed Form 1")).toBeInTheDocument(); }); + + it("keeps the row and does not refresh the lists when the delete request fails", async () => { + deleteRequest.mockImplementation( + () => () => () => Promise.reject(new Error("delete failed")) + ); + + const store = createRealStore([ + createManagedForm(1, { assignment_type: "Explicit" }) + ]); + + renderWithConfirmDialog(, { + store + }); + + const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + await act(async () => { + await userEvent.click(deleteButton); + }); + + expect( + await screen.findByText("general.are_you_sure") + ).toBeInTheDocument(); + + await act(async () => { + await userEvent.click( + await screen.findByRole("button", { + name: /general\.yes_delete|confirm/i + }) + ); + }); + + // Lets the rejected promise chain (deleteSponsorManagedForm's own + // .finally() plus handleManagedDelete's .catch()) settle. If the + // .catch() were removed, this would surface as an unhandled rejection + // and fail the test. + await act(async () => { + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + }); + + expect(screen.getByText("Managed Form 1")).toBeInTheDocument(); + expect(getSponsorManagedForms).toHaveBeenCalledTimes(1); // mount only, no refresh on failure + expect(getSponsorCustomizedForms).toHaveBeenCalledTimes(1); + }); }); describe("customized forms delete", () => {