From 45342d41faacfd42c043448ca5cac5d47855cb26 Mon Sep 17 00:00:00 2001 From: Nicolas Olmos Date: Mon, 12 Jan 2026 09:51:54 -0300 Subject: [PATCH 1/7] feat: add url fragment linking to tabs on edit sponsor page --- src/pages/sponsors/edit-sponsor-page.js | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/pages/sponsors/edit-sponsor-page.js b/src/pages/sponsors/edit-sponsor-page.js index 3a4e9a709..3c83919e8 100644 --- a/src/pages/sponsors/edit-sponsor-page.js +++ b/src/pages/sponsors/edit-sponsor-page.js @@ -46,6 +46,26 @@ import SponsorCartTab from "./sponsor-cart-tab"; import SponsorFormsManageItems from "./sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items"; import { SPONSOR_TABS } from "../../utils/constants"; +const tabsToFragmentMap = [ + "general", + "users", + "pages", + "media_uploads", + "forms", + "cart", + "purchases", + "badge_scans" +]; + +const getFragmentFromValue = (index) => tabsToFragmentMap[index]; + +const getTabFromUrlFragment = () => { + const result = tabsToFragmentMap.indexOf( + window.location.hash.replace("#", "") + ); + return result > -1 ? result : 0; +}; + const CustomTabPanel = (props) => { const { children, value, index, ...other } = props; @@ -97,16 +117,15 @@ const EditSponsorPage = (props) => { getExtraQuestionMeta } = props; - const [selectedTab, setSelectedTab] = useState( - location.pathname.includes("/sponsor-forms/") && - location.pathname.includes("/items") - ? SPONSOR_TABS.FORMS - : 0 - ); + const [selectedTab, setSelectedTab] = useState(getTabFromUrlFragment()); + + useEffect(() => { + setSelectedTab(getTabFromUrlFragment()); + }, [window.location.hash]); const handleTabChange = (event, newValue) => { setSelectedTab(newValue); - history.push(`/app/summits/${currentSummit.id}/sponsors/${entity.id}`); + window.location.hash = getFragmentFromValue(newValue); }; useEffect(() => { From 4893cb69faabc0a7a2bb24887f98e95a6e07fd41 Mon Sep 17 00:00:00 2001 From: Nicolas Olmos Date: Thu, 15 Jan 2026 11:44:26 -0300 Subject: [PATCH 2/7] chore: export default state to mock unit tests --- src/reducers/summits/current-summit-reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reducers/summits/current-summit-reducer.js b/src/reducers/summits/current-summit-reducer.js index 88d7cc951..3e229b606 100644 --- a/src/reducers/summits/current-summit-reducer.js +++ b/src/reducers/summits/current-summit-reducer.js @@ -214,7 +214,7 @@ const DEFAULT_PRINT_APP_MARKETING_SETTINGS = { PRINT_APP_HIDE_FIND_TICKET_BY_FULLNAME: { id: 0, value: false } }; -const DEFAULT_STATE = { +export const DEFAULT_STATE = { currentSummit: DEFAULT_ENTITY, errors: {}, loading: true, From b73855b25b1fd9391c801a95a61cfd3a093ef302 Mon Sep 17 00:00:00 2001 From: Nicolas Olmos Date: Thu, 15 Jan 2026 11:46:43 -0300 Subject: [PATCH 3/7] chore: export maps functions and components to unit test them * Add test id to make testing more feasible. --- src/pages/sponsors/edit-sponsor-page.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/sponsors/edit-sponsor-page.js b/src/pages/sponsors/edit-sponsor-page.js index 3c83919e8..01b2980d4 100644 --- a/src/pages/sponsors/edit-sponsor-page.js +++ b/src/pages/sponsors/edit-sponsor-page.js @@ -46,7 +46,7 @@ import SponsorCartTab from "./sponsor-cart-tab"; import SponsorFormsManageItems from "./sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items"; import { SPONSOR_TABS } from "../../utils/constants"; -const tabsToFragmentMap = [ +export const tabsToFragmentMap = [ "general", "users", "pages", @@ -57,16 +57,16 @@ const tabsToFragmentMap = [ "badge_scans" ]; -const getFragmentFromValue = (index) => tabsToFragmentMap[index]; +export const getFragmentFromValue = (index) => tabsToFragmentMap[index]; -const getTabFromUrlFragment = () => { +export const getTabFromUrlFragment = () => { const result = tabsToFragmentMap.indexOf( window.location.hash.replace("#", "") ); return result > -1 ? result : 0; }; -const CustomTabPanel = (props) => { +export const CustomTabPanel = (props) => { const { children, value, index, ...other } = props; return ( @@ -74,6 +74,7 @@ const CustomTabPanel = (props) => { role="tabpanel" hidden={value !== index} id={`simple-tabpanel-${index}`} + data-testid={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other} > From e5e4cecd85d5493d64cb9587216d5b7335ca370d Mon Sep 17 00:00:00 2001 From: Nicolas Olmos Date: Thu, 15 Jan 2026 11:47:18 -0300 Subject: [PATCH 4/7] chore: export default state to aid on mocking unit tests --- src/reducers/sponsors/sponsor-reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reducers/sponsors/sponsor-reducer.js b/src/reducers/sponsors/sponsor-reducer.js index ff98dd6e2..142bca97e 100644 --- a/src/reducers/sponsors/sponsor-reducer.js +++ b/src/reducers/sponsors/sponsor-reducer.js @@ -132,7 +132,7 @@ export const DEFAULT_ENTITY = { sponsorships_collection: DEFAULT_SPONSORHIPS_STATE }; -const DEFAULT_STATE = { +export const DEFAULT_STATE = { entity: DEFAULT_ENTITY, selectedSponsorship: null, errors: {} From f805edb6a5b99750b4ce94d846edba134ce2b3ab Mon Sep 17 00:00:00 2001 From: Nicolas Olmos Date: Thu, 15 Jan 2026 11:50:34 -0300 Subject: [PATCH 5/7] chore: unit test url fragment and page tabs deep linking behavior --- .../__tests__/edit-sponsor-page.test.js | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 src/pages/sponsors/__tests__/edit-sponsor-page.test.js diff --git a/src/pages/sponsors/__tests__/edit-sponsor-page.test.js b/src/pages/sponsors/__tests__/edit-sponsor-page.test.js new file mode 100644 index 000000000..4a47b5697 --- /dev/null +++ b/src/pages/sponsors/__tests__/edit-sponsor-page.test.js @@ -0,0 +1,226 @@ +import React from "react"; +import userEvent from "@testing-library/user-event"; +import { act, screen } from "@testing-library/react"; +import EditSponsorPage, { + getFragmentFromValue, + getTabFromUrlFragment +} from "../edit-sponsor-page"; +import { renderWithRedux } from "../../../utils/test-utils"; +import { + DEFAULT_STATE as currentSponsorDefaultState +} from "../../../reducers/sponsors/sponsor-reducer"; +import { + DEFAULT_ENTITY as defaultSummitEntity, + DEFAULT_STATE as currentSummitDefaultState +} from "../../../reducers/summits/current-summit-reducer"; + +global.window = { location: { pathname: "/sponsor-forms/items" } }; +jest.mock( + "../sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js" +); +jest.mock("../sponsor-users-list-per-sponsor/index.js"); + +describe("EditSponsorPage", () => { + describe("getFragmentFromValue", () => { + it("returns correct values", () => { + const result1 = getFragmentFromValue(0); + expect(result1).toBe("general"); + + const result2 = getFragmentFromValue(2); + expect(result2).toBe("pages"); + + const result3 = getFragmentFromValue(3); + expect(result3).toBe("media_uploads"); + + const result4 = getFragmentFromValue(7); + expect(result4).toBe("badge_scans"); + }); + }); + + describe("getTabFromUrlFragment", () => { + it("returns correct values for defined fragments", () => { + const newUrl1 = "#general"; + window.location.hash = newUrl1; + + const result1 = getTabFromUrlFragment(); + expect(result1).toBe(0); + + const newUrl2 = "#pages"; + window.location.hash = newUrl2; + + const result2 = getTabFromUrlFragment(); + expect(result2).toBe(2); + + const newUrl3 = "#media_uploads"; + window.location.hash = newUrl3; + + const result3 = getTabFromUrlFragment(); + expect(result3).toBe(3); + + const newUrl4 = "#badge_scans"; + window.location.hash = newUrl4; + + const result4 = getTabFromUrlFragment(); + expect(result4).toBe(7); + }); + + it("returns correct values for undefined fragments", () => { + const newUrl1 = "#generalx"; + window.location.hash = newUrl1; + + const result1 = getTabFromUrlFragment(); + expect(result1).toBe(0); + + const newUrl2 = "#frewawqfwedwdwqq"; + window.location.hash = newUrl2; + + const result2 = getTabFromUrlFragment(); + expect(result2).toBe(0); + + const newUrl3 = "#"; + window.location.hash = newUrl3; + + const result3 = getTabFromUrlFragment(); + expect(result3).toBe(0); + + const newUrl4 = ""; + window.location.hash = newUrl4; + + const result4 = getTabFromUrlFragment(); + expect(result4).toBe(0); + }); + }); + + describe("Component", () => { + const originalWindowLocation = window.location; + it("should change the url fragment on tab click", async () => { + delete window.location; + + Object.defineProperty(window, "location", { + configurable: true, + writable: true, + value: { + ...originalWindowLocation, + hash: "#general" + } + }); + + renderWithRedux( + , + { + initialState: { + currentSummitState: { + currentSummit: defaultSummitEntity, + ...currentSummitDefaultState + }, + loggedUserState: { + member: { + groups: {} + } + }, + currentSummitSponsorshipListState: { + sponsorships: [], + currentPage: 1, + lastPage: 1, + perPage: 100, + order: "order", + orderDir: 1, + totalSponsorships: 0 + }, + currentSponsorState: { + sponsorships: [], + ...currentSponsorDefaultState + } + } + } + ); + + const usersTabReference = screen.getByText("edit_sponsor.tab.forms"); + + await act(async () => { + await userEvent.click(usersTabReference); + }); + + expect(window.location.hash).toBe("forms"); + }); + + it("should change the tab rendered on fragment change", async () => { + delete window.location; + + Object.defineProperty(window, "location", { + configurable: true, + writable: true, + value: { + ...originalWindowLocation, + hash: "#general" + } + }); + + renderWithRedux( + , + { + initialState: { + currentSummitState: { + currentSummit: defaultSummitEntity, + ...currentSummitDefaultState + }, + loggedUserState: { + member: { + groups: {} + } + }, + currentSummitSponsorshipListState: { + sponsorships: [], + currentPage: 1, + lastPage: 1, + perPage: 100, + order: "order", + orderDir: 1, + totalSponsorships: 0 + }, + currentSponsorState: { + sponsorships: [], + ...currentSponsorDefaultState + } + } + } + ); + + const generalTabPanel = screen.getByTestId("simple-tabpanel-0"); + expect(generalTabPanel).toBeDefined(); + + delete window.location; + + Object.defineProperty(window, "location", { + configurable: true, + writable: true, + value: { + ...originalWindowLocation, + hash: "#users" + } + }); + + const usersTabPanel = screen.getByTestId("simple-tabpanel-1"); + expect(usersTabPanel).toBeDefined(); + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalWindowLocation + }); + }); + }); +}); From c2bb0efd33bcc8a9cd67b7aa233ec99b858f5472 Mon Sep 17 00:00:00 2001 From: Nicolas Olmos Date: Tue, 20 Jan 2026 16:50:36 -0300 Subject: [PATCH 6/7] chore: use event listener --- .../__tests__/edit-sponsor-page.test.js | 30 +------------------ src/pages/sponsors/edit-sponsor-page.js | 11 +++++-- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/pages/sponsors/__tests__/edit-sponsor-page.test.js b/src/pages/sponsors/__tests__/edit-sponsor-page.test.js index 4a47b5697..f6a1367d7 100644 --- a/src/pages/sponsors/__tests__/edit-sponsor-page.test.js +++ b/src/pages/sponsors/__tests__/edit-sponsor-page.test.js @@ -6,9 +6,7 @@ import EditSponsorPage, { getTabFromUrlFragment } from "../edit-sponsor-page"; import { renderWithRedux } from "../../../utils/test-utils"; -import { - DEFAULT_STATE as currentSponsorDefaultState -} from "../../../reducers/sponsors/sponsor-reducer"; +import { DEFAULT_STATE as currentSponsorDefaultState } from "../../../reducers/sponsors/sponsor-reducer"; import { DEFAULT_ENTITY as defaultSummitEntity, DEFAULT_STATE as currentSummitDefaultState @@ -63,32 +61,6 @@ describe("EditSponsorPage", () => { const result4 = getTabFromUrlFragment(); expect(result4).toBe(7); }); - - it("returns correct values for undefined fragments", () => { - const newUrl1 = "#generalx"; - window.location.hash = newUrl1; - - const result1 = getTabFromUrlFragment(); - expect(result1).toBe(0); - - const newUrl2 = "#frewawqfwedwdwqq"; - window.location.hash = newUrl2; - - const result2 = getTabFromUrlFragment(); - expect(result2).toBe(0); - - const newUrl3 = "#"; - window.location.hash = newUrl3; - - const result3 = getTabFromUrlFragment(); - expect(result3).toBe(0); - - const newUrl4 = ""; - window.location.hash = newUrl4; - - const result4 = getTabFromUrlFragment(); - expect(result4).toBe(0); - }); }); describe("Component", () => { diff --git a/src/pages/sponsors/edit-sponsor-page.js b/src/pages/sponsors/edit-sponsor-page.js index 01b2980d4..2bf43470c 100644 --- a/src/pages/sponsors/edit-sponsor-page.js +++ b/src/pages/sponsors/edit-sponsor-page.js @@ -63,7 +63,10 @@ export const getTabFromUrlFragment = () => { const result = tabsToFragmentMap.indexOf( window.location.hash.replace("#", "") ); - return result > -1 ? result : 0; + + if (result > -1) return result; + + if (!window.location.hash) window.location.hash = "#general"; }; export const CustomTabPanel = (props) => { @@ -121,8 +124,10 @@ const EditSponsorPage = (props) => { const [selectedTab, setSelectedTab] = useState(getTabFromUrlFragment()); useEffect(() => { - setSelectedTab(getTabFromUrlFragment()); - }, [window.location.hash]); + const onHashChange = () => setSelectedTab(getTabFromUrlFragment()); + window.addEventListener("hashchange", onHashChange); + return () => window.removeEventListener("hashchange", onHashChange); + }, []); const handleTabChange = (event, newValue) => { setSelectedTab(newValue); From e4fc3fe732d230bba22283fdc4181f354db4925b Mon Sep 17 00:00:00 2001 From: smarcet Date: Tue, 20 Jan 2026 17:41:05 -0300 Subject: [PATCH 7/7] chore: refactor --- src/pages/sponsors/edit-sponsor-page.js | 21 ++++++++++----------- yarn.lock | 11 +---------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/pages/sponsors/edit-sponsor-page.js b/src/pages/sponsors/edit-sponsor-page.js index 2bf43470c..d3101dc25 100644 --- a/src/pages/sponsors/edit-sponsor-page.js +++ b/src/pages/sponsors/edit-sponsor-page.js @@ -60,13 +60,10 @@ export const tabsToFragmentMap = [ export const getFragmentFromValue = (index) => tabsToFragmentMap[index]; export const getTabFromUrlFragment = () => { - const result = tabsToFragmentMap.indexOf( - window.location.hash.replace("#", "") - ); - + const currentHash = window.location.hash.replace("#", ""); + const result = tabsToFragmentMap.indexOf(currentHash); if (result > -1) return result; - - if (!window.location.hash) window.location.hash = "#general"; + return 0; }; export const CustomTabPanel = (props) => { @@ -123,17 +120,19 @@ const EditSponsorPage = (props) => { const [selectedTab, setSelectedTab] = useState(getTabFromUrlFragment()); + const handleTabChange = (event, newValue) => { + setSelectedTab(newValue); + window.location.hash = getFragmentFromValue(newValue); + }; + useEffect(() => { const onHashChange = () => setSelectedTab(getTabFromUrlFragment()); window.addEventListener("hashchange", onHashChange); + // default call + if (!window.location.hash) handleTabChange(null, getTabFromUrlFragment()); return () => window.removeEventListener("hashchange", onHashChange); }, []); - const handleTabChange = (event, newValue) => { - setSelectedTab(newValue); - window.location.hash = getFragmentFromValue(newValue); - }; - useEffect(() => { if (entity.id > 0) { getSponsorAdvertisements(entity.id); diff --git a/yarn.lock b/yarn.lock index e3e15533f..3b62ca6e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11048,16 +11048,7 @@ prop-types-extra@^1.0.1: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -prop-types@^15.8.1: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==