bui-themer: split + test + fixes
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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(
|
||||
<BuiThemePreview
|
||||
mode="light"
|
||||
styleObject={{
|
||||
'--bui-bg-surface-2': '#ffffff',
|
||||
'--bui-border': '#cccccc',
|
||||
'--bui-radius-2': '4px',
|
||||
'--bui-space-3': '12px',
|
||||
'--bui-fg-secondary': '#777777',
|
||||
'--bui-bg-solid': '#000000',
|
||||
'--bui-fg-solid': '#ffffff',
|
||||
'--bui-fg-primary': '#111111',
|
||||
'--bui-bg-surface-1': '#f5f5f5',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -89,12 +89,9 @@ export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) {
|
||||
/>
|
||||
<Checkbox label="Checkbox Option" />
|
||||
<RadioGroup label="Radio Group" orientation="horizontal">
|
||||
<Radio value="radio-option" />
|
||||
<Text variant="body-small">Option 1</Text>
|
||||
<Radio value="radio-option" />
|
||||
<Text variant="body-small">Option 2</Text>
|
||||
<Radio value="radio-option" />
|
||||
<Text variant="body-small">Option 3</Text>
|
||||
<Radio value="option-1">Option 1</Radio>
|
||||
<Radio value="option-2">Option 2</Radio>
|
||||
<Radio value="option-3">Option 3</Radio>
|
||||
</RadioGroup>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
|
||||
@@ -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 }) => (
|
||||
<ThemeProvider theme={createTheme()}>{children}</ThemeProvider>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
<TestApiProvider apis={apis}>
|
||||
<BuiThemerPage />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<TestApiProvider apis={apis}>
|
||||
<BuiThemerPage />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Light')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dark')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 }) => (
|
||||
<ThemeProvider theme={createTheme()}>{children}</ThemeProvider>
|
||||
),
|
||||
};
|
||||
|
||||
describe('MuiThemeExtractor', () => {
|
||||
it('invokes render prop with extracted theme', async () => {
|
||||
render(
|
||||
<MuiThemeExtractor appTheme={mockAppTheme}>
|
||||
{theme => <div>theme-palette-mode-{theme.palette.mode}</div>}
|
||||
</MuiThemeExtractor>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('theme-palette-mode-light')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<ThemeContent
|
||||
themeId="light"
|
||||
themeTitle="Light Theme"
|
||||
variant="light"
|
||||
muiTheme={muiTheme}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<ThemeContent
|
||||
themeId="light"
|
||||
themeTitle="Light Theme"
|
||||
variant="light"
|
||||
muiTheme={muiTheme}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Copy CSS' }));
|
||||
expect(writeText).toHaveBeenCalledWith(':root { --bui-color: #000; }');
|
||||
});
|
||||
|
||||
it('switches to Live Preview tab', async () => {
|
||||
await renderInTestApp(
|
||||
<ThemeContent
|
||||
themeId="light"
|
||||
themeTitle="Light Theme"
|
||||
variant="light"
|
||||
muiTheme={muiTheme}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Live Preview'));
|
||||
// The preview renders content from BuiThemePreview; basic smoke check:
|
||||
expect(screen.getByText('Theme Preview')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user