From 6dfa9e796cb1e6feff631d00c45c3313f8592a88 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 13 Sep 2025 18:53:02 +0200 Subject: [PATCH] bui-themer: split + test + fixes Signed-off-by: Patrik Oldsberg --- .../BuiThemerPage/BuiThemePreview.test.tsx | 68 +++++++++++++ .../BuiThemerPage/BuiThemePreview.tsx | 9 +- .../BuiThemerPage/BuiThemerPage.test.tsx | 82 +++++++++++++++ .../BuiThemerPage/MuiThemeExtractor.test.tsx | 41 ++++++++ .../BuiThemerPage/MuiThemeExtractor.tsx | 13 ++- .../BuiThemerPage/ThemeContent.test.tsx | 99 +++++++++++++++++++ 6 files changed, 303 insertions(+), 9 deletions(-) create mode 100644 plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx create mode 100644 plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.test.tsx create mode 100644 plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx create mode 100644 plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx new file mode 100644 index 0000000000..d42c648677 --- /dev/null +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Backstage Authors + * + * 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, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { BuiThemePreview } from './BuiThemePreview'; + +describe('BuiThemePreview', () => { + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + it('renders headings and sample components', () => { + const { container } = render( + , + ); + + expect(container.firstChild).toHaveAttribute('data-theme-mode', 'light'); + expect(screen.getByText('Theme Preview')).toBeInTheDocument(); + expect(screen.getByText('Button Variants')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Primary' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Secondary' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Tertiary' }), + ).toBeInTheDocument(); + expect(screen.getByText('Form Inputs')).toBeInTheDocument(); + expect(screen.getByText('Surface Variations')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.tsx index d04cc5eabd..b9c91fb032 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemePreview.tsx @@ -89,12 +89,9 @@ export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) { /> - - Option 1 - - Option 2 - - Option 3 + Option 1 + Option 2 + Option 3 diff --git a/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.test.tsx b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.test.tsx new file mode 100644 index 0000000000..49fe6f2a9a --- /dev/null +++ b/plugins/bui-themer/src/components/BuiThemerPage/BuiThemerPage.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2025 The Backstage Authors + * + * 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 { screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { BuiThemerPage } from './BuiThemerPage'; +import { appThemeApiRef } from '@backstage/core-plugin-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +function makeAppTheme(id: string, title: string, variant: 'light' | 'dark') { + return { + id, + title, + variant, + Provider: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + }; +} + +describe('BuiThemerPage', () => { + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + it('renders empty state when no themes installed', async () => { + const apis = [[appThemeApiRef, { getInstalledThemes: () => [] }]] as const; + await renderInTestApp( + + + , + ); + + expect( + screen.getByText(/No themes found. Please install some themes/i), + ).toBeInTheDocument(); + }); + + it('renders ThemeContent for each installed theme', async () => { + const themes = [ + makeAppTheme('light', 'Light', 'light'), + makeAppTheme('dark', 'Dark', 'dark'), + ]; + const apis = [ + [appThemeApiRef, { getInstalledThemes: () => themes }], + ] as const; + + await renderInTestApp( + + + , + ); + + expect(screen.getByText('Light')).toBeInTheDocument(); + expect(screen.getByText('Dark')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx b/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx new file mode 100644 index 0000000000..ea71f2ba33 --- /dev/null +++ b/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * 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, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { MuiThemeExtractor } from './MuiThemeExtractor'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +const mockAppTheme = { + id: 'mock', + title: 'Mock', + variant: 'light' as const, + Provider: ({ children }: { children: React.ReactNode }) => ( + {children} + ), +}; + +describe('MuiThemeExtractor', () => { + it('invokes render prop with extracted theme', async () => { + render( + + {theme =>
theme-palette-mode-{theme.palette.mode}
} +
, + ); + + expect(screen.getByText('theme-palette-mode-light')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.tsx b/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.tsx index b935720b9e..4cad158f53 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.tsx +++ b/plugins/bui-themer/src/components/BuiThemerPage/MuiThemeExtractor.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { AppTheme } from '@backstage/core-plugin-api'; import { Theme, useTheme } from '@mui/material/styles'; @@ -36,7 +36,14 @@ export function MuiThemeExtractor(props: { ); } -function MuiThemeExtractorInner(props: { setTheme: (theme: Theme) => void }) { - props.setTheme(useTheme()); +function MuiThemeExtractorInner({ + setTheme, +}: { + setTheme: (theme: Theme) => void; +}) { + const theme = useTheme(); + useEffect(() => { + setTheme(theme); + }, [theme, setTheme]); return null; } diff --git a/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx b/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx new file mode 100644 index 0000000000..720c46d12d --- /dev/null +++ b/plugins/bui-themer/src/components/BuiThemerPage/ThemeContent.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2025 The Backstage Authors + * + * 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 { screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { ThemeContent } from './ThemeContent'; +import { createTheme } from '@mui/material/styles'; +import { renderInTestApp } from '@backstage/test-utils'; + +jest.mock('./convertMuiToBuiTheme', () => ({ + convertMuiToBuiTheme: () => ({ + css: ':root { --bui-color: #000; }', + styleObject: { '--bui-color': '#000' }, + }), +})); + +describe('ThemeContent', () => { + const muiTheme = createTheme(); + + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + + it('renders title, variant and tabs', async () => { + await renderInTestApp( + , + ); + + expect(screen.getByText('Light Theme')).toBeInTheDocument(); + expect(screen.getByText('light theme')).toBeInTheDocument(); + expect(screen.getByText('Generated CSS')).toBeInTheDocument(); + expect(screen.getByText('Live Preview')).toBeInTheDocument(); + expect( + screen.getByText(':root { --bui-color: #000; }'), + ).toBeInTheDocument(); + }); + + it('handles Copy CSS click', async () => { + const writeText = jest.fn(); + Object.assign(window.navigator, { clipboard: { writeText } }); + + await renderInTestApp( + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'Copy CSS' })); + expect(writeText).toHaveBeenCalledWith(':root { --bui-color: #000; }'); + }); + + it('switches to Live Preview tab', async () => { + await renderInTestApp( + , + ); + + fireEvent.click(screen.getByText('Live Preview')); + // The preview renders content from BuiThemePreview; basic smoke check: + expect(screen.getByText('Theme Preview')).toBeInTheDocument(); + }); +});