From f0da13aefe856dd98ef6a6cbbe0e76cebe7849af Mon Sep 17 00:00:00 2001 From: saudademjj <128795969+saudademjj@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:52:39 +0800 Subject: [PATCH 1/2] fix(frontend): avoid PermissionsTabs crash before projects load --- frontend/web/components/PermissionsTabs.tsx | 7 +- .../__tests__/PermissionsTabs.test.ts | 108 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 frontend/web/components/__tests__/PermissionsTabs.test.ts diff --git a/frontend/web/components/PermissionsTabs.tsx b/frontend/web/components/PermissionsTabs.tsx index 9f2b07ea2811..4f292dc7a17d 100644 --- a/frontend/web/components/PermissionsTabs.tsx +++ b/frontend/web/components/PermissionsTabs.tsx @@ -40,7 +40,7 @@ const PermissionsTabs: FC = ({ }) => { const [searchProject, setSearchProject] = useState('') const [searchEnv, setSearchEnv] = useState('') - const projectData: Project[] = OrganisationStore.getProjects() + const projectData: Project[] | undefined = OrganisationStore.getProjects() const [project, setProject] = useState('') const [environments, setEnvironments] = useState([]) @@ -123,7 +123,10 @@ const PermissionsTabs: FC = ({ group={group} orgId={orgId} filter={searchProject} - mainItems={projectData.map((v) => ({ ...v, projectId: v.id }))} + mainItems={(projectData || []).map((v) => ({ + ...v, + projectId: v.id, + }))} role={role} level={'project'} ref={tabRef} diff --git a/frontend/web/components/__tests__/PermissionsTabs.test.ts b/frontend/web/components/__tests__/PermissionsTabs.test.ts new file mode 100644 index 000000000000..90ccac1cd6d8 --- /dev/null +++ b/frontend/web/components/__tests__/PermissionsTabs.test.ts @@ -0,0 +1,108 @@ +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import type { Project } from 'common/types/responses' +import PermissionsTabs from 'components/PermissionsTabs' +import OrganisationStore from 'common/stores/organisation-store' + +const mockRolePermissionsList = jest.fn(() => React.createElement('div')) + +jest.mock('common/stores/organisation-store', () => ({ + __esModule: true, + default: { + getProjects: jest.fn(), + }, +})) + +jest.mock('components/EditPermissions', () => ({ + EditPermissionsModal: () => React.createElement('div'), + __esModule: true, +})) + +jest.mock('components/navigation/TabMenu/Tabs', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => + React.createElement(React.Fragment, null, children), +})) + +jest.mock('components/navigation/TabMenu/TabItem', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => + React.createElement(React.Fragment, null, children), +})) + +jest.mock('components/base/forms/Input', () => ({ + __esModule: true, + default: () => React.createElement('input'), +})) + +jest.mock('components/RolePermissionsList', () => ({ + __esModule: true, + default: (props: unknown) => { + mockRolePermissionsList(props) + return React.createElement('div') + }, +})) + +jest.mock('components/ProjectFilter', () => ({ + __esModule: true, + default: () => React.createElement('div'), +})) + +jest.mock('components/PlanBasedAccess', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => + React.createElement(React.Fragment, null, children), +})) + +jest.mock('components/WarningMessage', () => ({ + __esModule: true, + default: () => React.createElement('div'), +})) + +jest.mock('common/utils/utils', () => ({ + __esModule: true, + default: { + safeParseEventValue: () => '', + }, +})) + +const getProjectsMock = OrganisationStore.getProjects as jest.MockedFunction< + typeof OrganisationStore.getProjects +> + +describe('PermissionsTabs', () => { + const globalWithRow = global as typeof globalThis & { Row?: unknown } + const originalRow = globalWithRow.Row + + beforeAll(() => { + globalWithRow.Row = ({ children, ...props }: Record) => + React.createElement('div', props, children) + }) + + afterAll(() => { + if (originalRow) { + globalWithRow.Row = originalRow + return + } + + delete globalWithRow.Row + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('uses an empty project list while organisation projects are loading', () => { + getProjectsMock.mockReturnValue(undefined as unknown as Project[]) + + expect(() => + renderToStaticMarkup(React.createElement(PermissionsTabs, { orgId: 1 })), + ).not.toThrow() + + const projectPermissionsCall = mockRolePermissionsList.mock.calls.find( + ([props]) => (props as { level?: string }).level === 'project', + )?.[0] as { mainItems?: unknown[] } | undefined + + expect(projectPermissionsCall?.mainItems).toEqual([]) + }) +}) From afb197d64428e4a09da3b6465092638981249e69 Mon Sep 17 00:00:00 2001 From: saudademjj <128795969+saudademjj@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:51:16 +0800 Subject: [PATCH 2/2] refactor: use nullish coalescing and drop brittle test - Replace || with ?? for projectData fallback - Remove PermissionsTabs.test.ts per review feedback --- frontend/web/components/PermissionsTabs.tsx | 2 +- .../__tests__/PermissionsTabs.test.ts | 108 ------------------ 2 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 frontend/web/components/__tests__/PermissionsTabs.test.ts diff --git a/frontend/web/components/PermissionsTabs.tsx b/frontend/web/components/PermissionsTabs.tsx index 4f292dc7a17d..3857234bb415 100644 --- a/frontend/web/components/PermissionsTabs.tsx +++ b/frontend/web/components/PermissionsTabs.tsx @@ -123,7 +123,7 @@ const PermissionsTabs: FC = ({ group={group} orgId={orgId} filter={searchProject} - mainItems={(projectData || []).map((v) => ({ + mainItems={(projectData ?? []).map((v) => ({ ...v, projectId: v.id, }))} diff --git a/frontend/web/components/__tests__/PermissionsTabs.test.ts b/frontend/web/components/__tests__/PermissionsTabs.test.ts deleted file mode 100644 index 90ccac1cd6d8..000000000000 --- a/frontend/web/components/__tests__/PermissionsTabs.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react' -import { renderToStaticMarkup } from 'react-dom/server' -import type { Project } from 'common/types/responses' -import PermissionsTabs from 'components/PermissionsTabs' -import OrganisationStore from 'common/stores/organisation-store' - -const mockRolePermissionsList = jest.fn(() => React.createElement('div')) - -jest.mock('common/stores/organisation-store', () => ({ - __esModule: true, - default: { - getProjects: jest.fn(), - }, -})) - -jest.mock('components/EditPermissions', () => ({ - EditPermissionsModal: () => React.createElement('div'), - __esModule: true, -})) - -jest.mock('components/navigation/TabMenu/Tabs', () => ({ - __esModule: true, - default: ({ children }: { children: React.ReactNode }) => - React.createElement(React.Fragment, null, children), -})) - -jest.mock('components/navigation/TabMenu/TabItem', () => ({ - __esModule: true, - default: ({ children }: { children: React.ReactNode }) => - React.createElement(React.Fragment, null, children), -})) - -jest.mock('components/base/forms/Input', () => ({ - __esModule: true, - default: () => React.createElement('input'), -})) - -jest.mock('components/RolePermissionsList', () => ({ - __esModule: true, - default: (props: unknown) => { - mockRolePermissionsList(props) - return React.createElement('div') - }, -})) - -jest.mock('components/ProjectFilter', () => ({ - __esModule: true, - default: () => React.createElement('div'), -})) - -jest.mock('components/PlanBasedAccess', () => ({ - __esModule: true, - default: ({ children }: { children: React.ReactNode }) => - React.createElement(React.Fragment, null, children), -})) - -jest.mock('components/WarningMessage', () => ({ - __esModule: true, - default: () => React.createElement('div'), -})) - -jest.mock('common/utils/utils', () => ({ - __esModule: true, - default: { - safeParseEventValue: () => '', - }, -})) - -const getProjectsMock = OrganisationStore.getProjects as jest.MockedFunction< - typeof OrganisationStore.getProjects -> - -describe('PermissionsTabs', () => { - const globalWithRow = global as typeof globalThis & { Row?: unknown } - const originalRow = globalWithRow.Row - - beforeAll(() => { - globalWithRow.Row = ({ children, ...props }: Record) => - React.createElement('div', props, children) - }) - - afterAll(() => { - if (originalRow) { - globalWithRow.Row = originalRow - return - } - - delete globalWithRow.Row - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('uses an empty project list while organisation projects are loading', () => { - getProjectsMock.mockReturnValue(undefined as unknown as Project[]) - - expect(() => - renderToStaticMarkup(React.createElement(PermissionsTabs, { orgId: 1 })), - ).not.toThrow() - - const projectPermissionsCall = mockRolePermissionsList.mock.calls.find( - ([props]) => (props as { level?: string }).level === 'project', - )?.[0] as { mainItems?: unknown[] } | undefined - - expect(projectPermissionsCall?.mainItems).toEqual([]) - }) -})