diff --git a/workspaces/scorecard/.changeset/many-frogs-count.md b/workspaces/scorecard/.changeset/many-frogs-count.md new file mode 100644 index 00000000000..b8b1f37aedd --- /dev/null +++ b/workspaces/scorecard/.changeset/many-frogs-count.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-scorecard': patch +--- + +Removed Backstage registration requirement for default Scorecard icons diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx index 106ab438045..59239a917a6 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx @@ -16,7 +16,19 @@ import { useApp } from '@backstage/core-plugin-api'; import Box from '@mui/material/Box'; import MuiIcon from '@mui/material/Icon'; +import type { SvgIconComponent } from '@mui/icons-material'; import type { SxProps, Theme } from '@mui/material/styles'; +import { + ScorecardSuccessStatusIcon, + ScorecardWarningStatusIcon, + ScorecardErrorStatusIcon, +} from '../..'; + +const builtInIcons: Record = { + scorecardSuccessStatusIcon: ScorecardSuccessStatusIcon, + scorecardWarningStatusIcon: ScorecardWarningStatusIcon, + scorecardErrorStatusIcon: ScorecardErrorStatusIcon, +}; /** * @public @@ -40,7 +52,7 @@ export const ScorecardIcon = ({ return null; } - const SystemIcon = app.getSystemIcon(icon); + const SystemIcon = app.getSystemIcon(icon) ?? builtInIcons[icon]; if (SystemIcon) { return ( diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/__tests__/ScorecardIcon.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/__tests__/ScorecardIcon.test.tsx new file mode 100644 index 00000000000..c1bcd972a60 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/__tests__/ScorecardIcon.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from '@testing-library/react'; + +import { ScorecardIcon } from '../ScorecardIcon'; + +const mockGetSystemIcon = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApp: () => ({ + getSystemIcon: mockGetSystemIcon, + }), +})); + +describe('ScorecardIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return null for empty icon string', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should use system icon when registered', () => { + const MockIcon = (props: any) => ( + + ); + mockGetSystemIcon.mockReturnValue(MockIcon); + + const { getByTestId } = render( + , + ); + + expect(mockGetSystemIcon).toHaveBeenCalledWith( + 'scorecardSuccessStatusIcon', + ); + expect(getByTestId('system-icon')).toBeInTheDocument(); + }); + + it('should fall back to built-in icon when system icon is not registered', () => { + mockGetSystemIcon.mockReturnValue(undefined); + + const { container } = render( + , + ); + + expect(mockGetSystemIcon).toHaveBeenCalledWith( + 'scorecardSuccessStatusIcon', + ); + expect( + container.querySelector('[data-testid="CheckCircleOutlineIcon"]'), + ).toBeInTheDocument(); + }); + + it('should fall back to built-in warning icon', () => { + mockGetSystemIcon.mockReturnValue(undefined); + + const { container } = render( + , + ); + + expect( + container.querySelector('[data-testid="WarningAmberIcon"]'), + ).toBeInTheDocument(); + }); + + it('should fall back to built-in error icon', () => { + mockGetSystemIcon.mockReturnValue(undefined); + + const { container } = render( + , + ); + + expect( + container.querySelector('[data-testid="DangerousOutlinedIcon"]'), + ).toBeInTheDocument(); + }); + + it('should prefer system icon over built-in icon', () => { + const OverrideIcon = (props: any) => ( + + ); + mockGetSystemIcon.mockReturnValue(OverrideIcon); + + const { getByTestId, container } = render( + , + ); + + expect(getByTestId('override-icon')).toBeInTheDocument(); + expect( + container.querySelector('[data-testid="CheckCircleOutlineIcon"]'), + ).not.toBeInTheDocument(); + }); + + it('should render URL-based icon as image', () => { + mockGetSystemIcon.mockReturnValue(undefined); + + const { container } = render( + , + ); + + const img = container.querySelector('img'); + expect(img).toBeInTheDocument(); + expect(img?.getAttribute('src')).toBe('https://example.com/icon.png'); + }); + + it('should render unknown icon string as Material Design ligature', () => { + mockGetSystemIcon.mockReturnValue(undefined); + + const { container } = render(); + + const muiIcon = container.querySelector('.material-icons-outlined'); + expect(muiIcon).toBeInTheDocument(); + expect(muiIcon?.textContent).toBe('settings'); + }); +});