From 9d904514e6c87fee9e6089a1217c19b0bd38388d Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 22 Apr 2026 17:40:46 -0300 Subject: [PATCH 1/4] chore: implement polling and current summit filter --- src/actions/base-actions.js | 6 ++ src/actions/sponsor-users-actions.js | 59 +++++++++++++++++-- src/i18n/en.json | 4 +- .../components/import-users-popup.js | 16 +++-- .../sponsor-users-list-per-sponsor/index.js | 31 ++++++++-- .../sponsors/sponsor-users-list-reducer.js | 28 ++++++++- src/utils/constants.js | 9 +++ 7 files changed, 135 insertions(+), 18 deletions(-) diff --git a/src/actions/base-actions.js b/src/actions/base-actions.js index 6f488255b..9e1640563 100644 --- a/src/actions/base-actions.js +++ b/src/actions/base-actions.js @@ -51,3 +51,9 @@ export const snackbarSuccessHandler = (message) => (dispatch, state) => dispatch, state ); + +export const snackbarErrorMsg = (message) => (dispatch, state) => + setSnackbarMessage({ ...message, type: "error", code: CODE_200 })( + dispatch, + state + ); diff --git a/src/actions/sponsor-users-actions.js b/src/actions/sponsor-users-actions.js index 92afda7d2..df384ee7e 100644 --- a/src/actions/sponsor-users-actions.js +++ b/src/actions/sponsor-users-actions.js @@ -30,15 +30,23 @@ import { DEFAULT_ORDER_DIR, DEFAULT_PER_PAGE, DUMMY_ACTION, + IMPORT_SPONSOR_USERS_STATUS, SPONSOR_USER_ASSIGNMENT_TYPE } from "../utils/constants"; -import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; +import { + snackbarErrorHandler, + snackbarErrorMsg, + snackbarSuccessHandler +} from "./base-actions"; export const RECEIVE_SPONSOR_USER_GROUPS = "RECEIVE_SPONSOR_USER_GROUPS"; export const REQUEST_SPONSOR_USER_REQUESTS = "REQUEST_SPONSOR_USER_REQUESTS"; export const RECEIVE_SPONSOR_USER_REQUESTS = "RECEIVE_SPONSOR_USER_REQUESTS"; export const REQUEST_SPONSOR_USERS = "REQUEST_SPONSOR_USERS"; export const RECEIVE_SPONSOR_USERS = "RECEIVE_SPONSOR_USERS"; +export const IMPORT_SPONSOR_USERS_TRIGGERED = "IMPORT_SPONSOR_USERS_TRIGGERED"; +export const RECEIVE_SPONSOR_USERS_IMPORT_STATUS = + "RECEIVE_SPONSOR_USERS_IMPORT_STATUS"; export const SPONSOR_USER_ADDED = "SPONSOR_USER_ADDED"; export const SPONSOR_USER_REQUEST_ACCEPTED = "SPONSOR_USER_REQUEST_ACCEPTED"; export const SPONSOR_USER_REQUEST_DELETED = "SPONSOR_USER_REQUEST_DELETED"; @@ -537,17 +545,57 @@ export const sendSponsorUserInvite = (email) => async (dispatch, getState) => { }); }; -export const fetchSponsorUsersBySummit = async (summitId, companyId, page) => { +export const fetchSponsorUsersBySummit = async ( + currentSummitId, + summitId, + companyId, + page +) => { const accessToken = await getAccessTokenSafely(); return fetch( - `${window.SPONSOR_USERS_API_URL}/api/v1/sponsor-users?filter[]=summit_id==${summitId}&filter[]=company_id==${companyId}&access_token=${accessToken}&page=${page}&per_page=10&order=first_name&order_dir=asc` + `${window.SPONSOR_USERS_API_URL}/api/v1/sponsor-users?filter[]=not_summit_id==${currentSummitId}&filter[]=summit_id==${summitId}&filter[]=company_id==${companyId}&access_token=${accessToken}&page=${page}&per_page=10&order=first_name&order_dir=asc` ) .then(fetchResponseHandler) .then((json) => json) .catch(fetchErrorHandler); }; +export const trackImportSponsorUsers = () => async (dispatch, getState) => { + const { sponsorUserListState } = getState(); + const { tasks } = sponsorUserListState; + + const accessToken = await getAccessTokenSafely(); + const params = { + access_token: accessToken + }; + + tasks.forEach((taskId) => { + getRequest( + null, + createAction(RECEIVE_SPONSOR_USERS_IMPORT_STATUS), + `${window.SPONSOR_USERS_API_URL}/api/v1/tasks/${taskId}`, + authErrorHandler + )(params)(dispatch).then((res) => { + if (res.status === IMPORT_SPONSOR_USERS_STATUS.SUCCESS) { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("sponsor_users.import_users.success") + }) + ); + } else if (res.status === IMPORT_SPONSOR_USERS_STATUS.FAILURE) { + dispatch( + snackbarErrorMsg({ + title: T.translate("sponsor_users.import_users.fail"), + html: res.error + }) + ); + } + }); + }); +}; + export const importSponsorUsers = (sponsorId, companyId, summitId, userIds) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); @@ -575,17 +623,16 @@ export const importSponsorUsers = return postRequest( null, - createAction(DUMMY_ACTION), + createAction(IMPORT_SPONSOR_USERS_TRIGGERED), `${window.SPONSOR_USERS_API_URL}/api/v1/sponsor-users/import`, payload, snackbarErrorHandler )(params)(dispatch) .then(() => { - dispatch(stopLoading()); dispatch( snackbarSuccessHandler({ title: T.translate("general.success"), - html: T.translate("sponsor_users.import_users.success") + html: T.translate("sponsor_users.import_users.running") }) ); }) diff --git a/src/i18n/en.json b/src/i18n/en.json index 4599c5b52..c8da6b714 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2876,7 +2876,9 @@ "select_users": "Select Users", "select_all_users": "Select All Users", "import_users": "Import Users", - "success": "Users imported successfully." + "running": "Users import running on background.", + "success": "Users imported successfully.", + "fail": "Import failed" } }, "sponsorship_list": { diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/components/import-users-popup.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/components/import-users-popup.js index fd69e7111..3a6f8259d 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/components/import-users-popup.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/components/import-users-popup.js @@ -35,18 +35,22 @@ const ImportUsersPopup = ({ useEffect(() => { if (selectedSummit) { - fetchSponsorUsersBySummit(selectedSummit, companyId, 1).then( - (userData) => { - setUserOptions(userData); - setSelectedUsers([]); - } - ); + fetchSponsorUsersBySummit( + currentSummit.id, + selectedSummit, + companyId, + 1 + ).then((userData) => { + setUserOptions(userData); + setSelectedUsers([]); + }); } }, [selectedSummit]); const handleLoadMoreUsers = () => { if (userOptions.current_page < userOptions.last_page) { fetchSponsorUsersBySummit( + currentSummit.id, selectedSummit, companyId, userOptions.current_page + 1 diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/index.js index 2dce69bb6..cc4ff6e3e 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/index.js @@ -11,7 +11,7 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; import { Box, Button, Grid2 } from "@mui/material"; @@ -20,7 +20,8 @@ import SaveAltIcon from "@mui/icons-material/SaveAlt"; import { deleteSponsorUser, getSponsorUserRequests, - getSponsorUsers + getSponsorUsers, + trackImportSponsorUsers } from "../../../../../actions/sponsor-users-actions"; import SearchInput from "../../../../../components/mui/search-input"; import UsersTable from "../../../sponsor-users-list-page/components/users-table"; @@ -30,18 +31,23 @@ import NewUserPopup from "./components/new-user-popup"; import ProcessRequestPopup from "./components/process-request-popup"; import ImportUsersPopup from "./components/import-users-popup"; import EditUserPopup from "./components/edit-user-popup"; +import { TEN_SECONDS_IN_MILLISECONDS } from "../../../../../utils/constants"; const SponsorUsersListPerSponsorPage = ({ sponsor, requests, users, term, + importTasks, getSponsorUserRequests, getSponsorUsers, - deleteSponsorUser + deleteSponsorUser, + trackImportSponsorUsers }) => { const [openPopup, setOpenPopup] = useState(null); const [userEdit, setUserEdit] = useState(null); + const importIntervalRef = useRef(null); + const hasImportTasks = !!importTasks?.length; const sponsorId = sponsor?.id; const companyId = sponsor?.company?.id; @@ -50,6 +56,22 @@ const SponsorUsersListPerSponsorPage = ({ if (sponsorId) getSponsorUsers(sponsorId); }, [sponsorId, companyId]); + useEffect(() => { + if (hasImportTasks && !importIntervalRef.current) { + importIntervalRef.current = setInterval( + () => trackImportSponsorUsers(), + TEN_SECONDS_IN_MILLISECONDS + ); + } else if (!hasImportTasks && importIntervalRef.current) { + clearInterval(importIntervalRef.current); + importIntervalRef.current = null; + } + return () => { + clearInterval(importIntervalRef.current); + importIntervalRef.current = null; + }; + }, [hasImportTasks]); + const handleSearch = (searchTerm) => { getSponsorUsers(sponsor.id, searchTerm); }; @@ -168,5 +190,6 @@ const mapStateToProps = ({ sponsorUsersListState, currentSponsorState }) => ({ export default connect(mapStateToProps, { getSponsorUserRequests, getSponsorUsers, - deleteSponsorUser + deleteSponsorUser, + trackImportSponsorUsers })(SponsorUsersListPerSponsorPage); diff --git a/src/reducers/sponsors/sponsor-users-list-reducer.js b/src/reducers/sponsors/sponsor-users-list-reducer.js index 3f234066d..42645d268 100644 --- a/src/reducers/sponsors/sponsor-users-list-reducer.js +++ b/src/reducers/sponsors/sponsor-users-list-reducer.js @@ -14,17 +14,21 @@ import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; import { epochToMoment } from "openstack-uicore-foundation/lib/utils/methods"; import { + IMPORT_SPONSOR_USERS_TRIGGERED, RECEIVE_SPONSOR_USER_GROUPS, RECEIVE_SPONSOR_USER_REQUESTS, RECEIVE_SPONSOR_USERS, + RECEIVE_SPONSOR_USERS_IMPORT_STATUS, REQUEST_SPONSOR_USER_REQUESTS, - REQUEST_SPONSOR_USERS, + REQUEST_SPONSOR_USERS } from "../../actions/sponsor-users-actions"; import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions"; +import { IMPORT_SPONSOR_USERS_STATUS } from "../../utils/constants"; const DEFAULT_STATE = { term: "", userGroups: [], + importTasks: [], requests: { items: [], order: "id", @@ -143,6 +147,28 @@ const sponsorUsersListReducer = (state = DEFAULT_STATE, action) => { } }; } + case IMPORT_SPONSOR_USERS_TRIGGERED: { + const { task_id } = payload.response; + return { + ...state, + importTasks: [...state.importTasks, task_id] + }; + } + case RECEIVE_SPONSOR_USERS_IMPORT_STATUS: { + const { status, task_id } = payload.response; + let {importTasks} = state; + + if ( + [ + IMPORT_SPONSOR_USERS_STATUS.SUCCESS, + IMPORT_SPONSOR_USERS_STATUS.FAILURE + ].includes(status) + ) { + importTasks = state.importTasks.filter((t) => t === task_id); + } + + return { ...state, importTasks }; + } default: return state; } diff --git a/src/utils/constants.js b/src/utils/constants.js index 5c7b72fcd..4aeb71930 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -66,6 +66,8 @@ export const REG_LITE_BOOLEAN_SETTINGS = [ "REG_LITE_SHOW_COMPANY_INPUT_DEFAULT_OPTIONS" ]; +export const TEN_SECONDS_IN_MILLISECONDS = 10000; + export const NOTIFICATION_TIMEOUT = 2000; export const DUMMY_ACTION = "DUMMY_ACTION"; @@ -325,3 +327,10 @@ export const SPONSOR_CART_STATUS = { OPEN: "Open", CHECKED_OUT: "CheckedOut" }; + +export const IMPORT_SPONSOR_USERS_STATUS = { + PENDING: "PENDING", + STARTED: "STARTED", + SUCCESS: "SUCCESS", + FAILURE: "FAILURE" +}; From 20fe14573a5c1f462638e111dd30dd4df72d7e07 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 23 Apr 2026 16:31:09 -0300 Subject: [PATCH 2/4] chore: integrate API changes - WIP --- src/actions/sponsor-users-actions.js | 37 +++++++++++++------ .../sponsors/sponsor-users-list-reducer.js | 4 +- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/actions/sponsor-users-actions.js b/src/actions/sponsor-users-actions.js index df384ee7e..690b7cdfe 100644 --- a/src/actions/sponsor-users-actions.js +++ b/src/actions/sponsor-users-actions.js @@ -562,33 +562,42 @@ export const fetchSponsorUsersBySummit = async ( }; export const trackImportSponsorUsers = () => async (dispatch, getState) => { - const { sponsorUserListState } = getState(); - const { tasks } = sponsorUserListState; + const { sponsorUsersListState } = getState(); + const { importTasks } = sponsorUsersListState; const accessToken = await getAccessTokenSafely(); const params = { access_token: accessToken }; - tasks.forEach((taskId) => { + importTasks.forEach((taskId) => { getRequest( null, createAction(RECEIVE_SPONSOR_USERS_IMPORT_STATUS), `${window.SPONSOR_USERS_API_URL}/api/v1/tasks/${taskId}`, authErrorHandler - )(params)(dispatch).then((res) => { - if (res.status === IMPORT_SPONSOR_USERS_STATUS.SUCCESS) { + )(params)(dispatch).then(({ response }) => { + if (response.status === IMPORT_SPONSOR_USERS_STATUS.SUCCESS) { dispatch( snackbarSuccessHandler({ title: T.translate("general.success"), html: T.translate("sponsor_users.import_users.success") }) ); - } else if (res.status === IMPORT_SPONSOR_USERS_STATUS.FAILURE) { + + if (response.result.errors.length > 0) { + dispatch( + snackbarErrorMsg({ + title: T.translate("sponsor_users.import_users.fail"), + html: response.result.errors.map((e) => e.error).join("
") + }) + ); + } + } else if (response.status === IMPORT_SPONSOR_USERS_STATUS.FAILURE) { dispatch( snackbarErrorMsg({ title: T.translate("sponsor_users.import_users.fail"), - html: res.error + html: response.error }) ); } @@ -597,7 +606,10 @@ export const trackImportSponsorUsers = () => async (dispatch, getState) => { }; export const importSponsorUsers = - (sponsorId, companyId, summitId, userIds) => async (dispatch) => { + (targetSponsorId, targetCompanyId, sourceSummitId, userIds) => + async (dispatch, getState) => { + const { currentSummitState } = getState(); + const { currentSummit } = currentSummitState; const accessToken = await getAccessTokenSafely(); let payload; @@ -610,13 +622,14 @@ export const importSponsorUsers = if (userIds === "all") { payload = { apply_to_all_users: true, - source_company_id: companyId, - source_summit_id: summitId + source_company_id: targetCompanyId, // target and source companyId is the same + source_summit_id: sourceSummitId, + target_summit_id: currentSummit.id }; } else { payload = { - summit_id: summitId, - sponsor_id: sponsorId, + target_summit_id: currentSummit.id, + sponsor_id: targetSponsorId, user_ids: userIds }; } diff --git a/src/reducers/sponsors/sponsor-users-list-reducer.js b/src/reducers/sponsors/sponsor-users-list-reducer.js index 42645d268..0b50c5d1f 100644 --- a/src/reducers/sponsors/sponsor-users-list-reducer.js +++ b/src/reducers/sponsors/sponsor-users-list-reducer.js @@ -156,7 +156,7 @@ const sponsorUsersListReducer = (state = DEFAULT_STATE, action) => { } case RECEIVE_SPONSOR_USERS_IMPORT_STATUS: { const { status, task_id } = payload.response; - let {importTasks} = state; + let { importTasks } = state; if ( [ @@ -164,7 +164,7 @@ const sponsorUsersListReducer = (state = DEFAULT_STATE, action) => { IMPORT_SPONSOR_USERS_STATUS.FAILURE ].includes(status) ) { - importTasks = state.importTasks.filter((t) => t === task_id); + importTasks = state.importTasks.filter((t) => t !== task_id); } return { ...state, importTasks }; From 07413c7adc7611188ca452df733220d1f7d4556b Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 23 Apr 2026 17:22:29 -0300 Subject: [PATCH 3/4] chore: add tests --- ...ponsor-users-list-per-sponsor-page.test.js | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/__tests__/sponsor-users-list-per-sponsor-page.test.js diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/__tests__/sponsor-users-list-per-sponsor-page.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/__tests__/sponsor-users-list-per-sponsor-page.test.js new file mode 100644 index 000000000..08fea28f8 --- /dev/null +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-users-list-per-sponsor/__tests__/sponsor-users-list-per-sponsor-page.test.js @@ -0,0 +1,395 @@ +import React from "react"; +import { act, fireEvent, render, screen } from "@testing-library/react"; +import SponsorUsersListPerSponsorPage from "../index"; +import { renderWithRedux } from "../../../../../../utils/test-utils"; +import { + getSponsorUserRequests, + getSponsorUsers, + trackImportSponsorUsers +} from "../../../../../../actions/sponsor-users-actions"; + +// ── Mocks ────────────────────────────────────────────────────────────────────── + +jest.mock("i18n-react/dist/i18n-react", () => ({ + translate: (key) => key +})); + +jest.mock("../../../../../../actions/sponsor-users-actions", () => ({ + getSponsorUsers: jest.fn(() => ({ type: "MOCK_ACTION" })), + getSponsorUserRequests: jest.fn(() => ({ type: "MOCK_ACTION" })), + deleteSponsorUser: jest.fn(() => ({ type: "MOCK_ACTION" })), + trackImportSponsorUsers: jest.fn(() => ({ type: "MOCK_ACTION" })) +})); + +jest.mock( + "../../../../sponsor-users-list-page/components/users-table", + () => () =>
+); + +jest.mock( + "../../../../../../components/mui/search-input", + () => + function SearchInputMock({ onSearch }) { + return ( + onSearch(e.target.value)} + /> + ); + } +); + +jest.mock("../../../../../../components/mui/custom-alert", () => () => ( +
+)); + +jest.mock( + "../../../../../../components/mui/chip-notify", + () => + function ChipNotifyMock({ label, onClick }) { + return ( + + ); + } +); + +jest.mock( + "../components/new-user-popup", + () => + function NewUserPopupMock({ onClose }) { + return ( +
+ +
+ ); + } +); + +jest.mock( + "../components/edit-user-popup", + () => + function EditUserPopupMock({ onClose }) { + return ( +
+ +
+ ); + } +); + +jest.mock( + "../components/process-request-popup", + () => + function ProcessRequestPopupMock({ onClose }) { + return ( +
+ +
+ ); + } +); + +jest.mock( + "../components/import-users-popup", + () => + function ImportUsersPopupMock({ onClose }) { + return ( +
+ +
+ ); + } +); + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +const DEFAULT_SPONSOR = { id: 123, company: { id: 456 } }; + +const DEFAULT_USERS = { + items: [], + order: "id", + orderDir: 1, + currentPage: 1, + lastPage: 1, + perPage: 10, + totalCount: 5 +}; + +const DEFAULT_REQUESTS = { + items: [], + order: "id", + orderDir: 1, + currentPage: 1, + lastPage: 1, + perPage: 10, + totalCount: 0 +}; + +const buildState = ({ + sponsor = DEFAULT_SPONSOR, + importTasks = [], + requests = DEFAULT_REQUESTS, + users = DEFAULT_USERS, + term = "" +} = {}) => ({ + currentSponsorState: { entity: sponsor }, + sponsorUsersListState: { term, userGroups: [], importTasks, requests, users } +}); + +const renderPage = (stateOverrides = {}) => + renderWithRedux(, { + initialState: buildState(stateOverrides) + }); + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe("SponsorUsersListPerSponsorPage", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ── Data fetching on mount ───────────────────────────────────────────────── + + describe("data fetching on mount", () => { + it("fetches sponsor users on mount using the sponsor id", () => { + renderPage(); + + expect(getSponsorUsers).toHaveBeenCalledWith(DEFAULT_SPONSOR.id); + }); + + it("fetches access requests on mount using the company id", () => { + renderPage(); + + expect(getSponsorUserRequests).toHaveBeenCalledWith( + DEFAULT_SPONSOR.company.id + ); + }); + + it("does not fetch users when sponsor id is absent", () => { + renderPage({ sponsor: { company: { id: 456 } } }); + + expect(getSponsorUsers).not.toHaveBeenCalled(); + }); + + it("does not fetch requests when company id is absent", () => { + renderPage({ sponsor: { id: 123 } }); + + expect(getSponsorUserRequests).not.toHaveBeenCalled(); + }); + }); + + // ── Access request chip notification ────────────────────────────────────── + + describe("access request notification", () => { + it("shows the chip when there are pending access requests", () => { + renderPage({ requests: { ...DEFAULT_REQUESTS, totalCount: 3 } }); + + expect(screen.getByTestId("chip-notify")).toBeInTheDocument(); + }); + + it("hides the chip when there are no pending access requests", () => { + renderPage({ requests: { ...DEFAULT_REQUESTS, totalCount: 0 } }); + + expect(screen.queryByTestId("chip-notify")).not.toBeInTheDocument(); + }); + + it("includes the request count in the chip label", () => { + renderPage({ requests: { ...DEFAULT_REQUESTS, totalCount: 7 } }); + + expect(screen.getByTestId("chip-notify")).toHaveTextContent("7"); + }); + }); + + // ── Polling for import tasks ─────────────────────────────────────────────── + + describe("polling for import tasks", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("starts polling every 10 seconds when import tasks are present", () => { + renderPage({ importTasks: [{ id: 1, status: "running" }] }); + + expect(trackImportSponsorUsers).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(10000); + }); + expect(trackImportSponsorUsers).toHaveBeenCalledTimes(1); + + act(() => { + jest.advanceTimersByTime(10000); + }); + expect(trackImportSponsorUsers).toHaveBeenCalledTimes(2); + }); + + it("does not poll when there are no import tasks", () => { + renderPage({ importTasks: [] }); + + act(() => { + jest.advanceTimersByTime(30000); + }); + + expect(trackImportSponsorUsers).not.toHaveBeenCalled(); + }); + + it("clears the polling interval when the component unmounts", () => { + const { unmount } = renderPage({ + importTasks: [{ id: 1, status: "running" }] + }); + + unmount(); + + act(() => { + jest.advanceTimersByTime(30000); + }); + + expect(trackImportSponsorUsers).not.toHaveBeenCalled(); + }); + + it("stops polling when import tasks are cleared", () => { + // Use the inner component directly so we can update props via rerender + // without Provider/connect wrapping interfering with effect cleanup timing. + const UnconnectedPage = SponsorUsersListPerSponsorPage.WrappedComponent; + const trackMock = jest.fn(() => ({ type: "MOCK_ACTION" })); + const sharedProps = { + sponsor: DEFAULT_SPONSOR, + users: DEFAULT_USERS, + requests: DEFAULT_REQUESTS, + term: "", + getSponsorUsers: jest.fn(), + getSponsorUserRequests: jest.fn(), + deleteSponsorUser: jest.fn(), + trackImportSponsorUsers: trackMock + }; + + const { rerender } = render( + + ); + + act(() => { + jest.advanceTimersByTime(10000); + }); + expect(trackMock).toHaveBeenCalledTimes(1); + + // Update props to simulate tasks completing + rerender(); + + act(() => { + jest.advanceTimersByTime(30000); + }); + + // Still only 1 call — interval was cleared when tasks were removed + expect(trackMock).toHaveBeenCalledTimes(1); + }); + }); + + // ── Popup behavior ───────────────────────────────────────────────────────── + + describe("popup behavior", () => { + it("opens the import popup when the import button is clicked", () => { + renderPage(); + + expect( + screen.queryByTestId("import-users-popup") + ).not.toBeInTheDocument(); + + fireEvent.click( + screen.getByText("sponsor_users.import_user").closest("button") + ); + + expect(screen.getByTestId("import-users-popup")).toBeInTheDocument(); + }); + + it("closes the import popup when its onClose is triggered", () => { + renderPage(); + + fireEvent.click( + screen.getByText("sponsor_users.import_user").closest("button") + ); + fireEvent.click(screen.getByTestId("close-import-users-popup")); + + expect( + screen.queryByTestId("import-users-popup") + ).not.toBeInTheDocument(); + }); + + it("opens the new user popup when the add user button is clicked", () => { + renderPage(); + + expect(screen.queryByTestId("new-user-popup")).not.toBeInTheDocument(); + + fireEvent.click( + screen.getByText("sponsor_users.add_user").closest("button") + ); + + expect(screen.getByTestId("new-user-popup")).toBeInTheDocument(); + }); + + it("closes the new user popup when its onClose is triggered", () => { + renderPage(); + + fireEvent.click( + screen.getByText("sponsor_users.add_user").closest("button") + ); + fireEvent.click(screen.getByTestId("close-new-user-popup")); + + expect(screen.queryByTestId("new-user-popup")).not.toBeInTheDocument(); + }); + + it("opens the process request popup when the chip notify is clicked", () => { + renderPage({ requests: { ...DEFAULT_REQUESTS, totalCount: 2 } }); + + expect( + screen.queryByTestId("process-request-popup") + ).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId("chip-notify")); + + expect(screen.getByTestId("process-request-popup")).toBeInTheDocument(); + }); + + it("closes the process request popup when its onClose is triggered", () => { + renderPage({ requests: { ...DEFAULT_REQUESTS, totalCount: 2 } }); + + fireEvent.click(screen.getByTestId("chip-notify")); + fireEvent.click(screen.getByTestId("close-process-request-popup")); + + expect( + screen.queryByTestId("process-request-popup") + ).not.toBeInTheDocument(); + }); + }); + + // ── Search ───────────────────────────────────────────────────────────────── + + describe("search", () => { + it("calls getSponsorUsers with the search term when the search input changes", () => { + renderPage(); + jest.clearAllMocks(); + + fireEvent.change(screen.getByTestId("search-input"), { + target: { value: "alice" } + }); + + expect(getSponsorUsers).toHaveBeenCalledWith(DEFAULT_SPONSOR.id, "alice"); + }); + }); +}); From 7b75243a042a5d818d425b10e1ebcd6b8d5735d5 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 24 Apr 2026 17:03:41 -0300 Subject: [PATCH 4/4] chore: refresh users on success --- src/actions/sponsor-users-actions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/actions/sponsor-users-actions.js b/src/actions/sponsor-users-actions.js index 690b7cdfe..ed7e87266 100644 --- a/src/actions/sponsor-users-actions.js +++ b/src/actions/sponsor-users-actions.js @@ -562,8 +562,9 @@ export const fetchSponsorUsersBySummit = async ( }; export const trackImportSponsorUsers = () => async (dispatch, getState) => { - const { sponsorUsersListState } = getState(); + const { sponsorUsersListState, currentSponsorState } = getState(); const { importTasks } = sponsorUsersListState; + const { entity: sponsor } = currentSponsorState; const accessToken = await getAccessTokenSafely(); const params = { @@ -593,6 +594,8 @@ export const trackImportSponsorUsers = () => async (dispatch, getState) => { }) ); } + + dispatch(getSponsorUsers(sponsor.id)); } else if (response.status === IMPORT_SPONSOR_USERS_STATUS.FAILURE) { dispatch( snackbarErrorMsg({