Enhanced testing capabilities including log collection, async rendering, and breakpoint mocking for comprehensive test coverage. These utilities extend React Testing Library with Backstage-specific functionality.
Captures console output during test execution for verification and debugging.
/**
* Captures console logs during async callback execution
* @param callback - Async function to execute while collecting logs
* @returns Promise resolving to collected logs by type
*/
function withLogCollector(callback: () => Promise<void>): Promise<CollectedLogs<LogFuncs>>;
/**
* Captures console logs during synchronous callback execution
* @param callback - Synchronous function to execute while collecting logs
* @returns Collected logs by type
*/
function withLogCollector(callback: () => void): CollectedLogs<LogFuncs>;
/**
* Captures specific log types during async callback execution
* @param logsToCollect - Array of log types to capture
* @param callback - Async function to execute while collecting logs
* @returns Promise resolving to collected logs for specified types
*/
function withLogCollector<T extends readonly string[]>(
logsToCollect: T,
callback: () => Promise<void>
): Promise<CollectedLogs<T>>;
/**
* Captures specific log types during synchronous callback execution
* @param logsToCollect - Array of log types to capture
* @param callback - Synchronous function to execute while collecting logs
* @returns Collected logs for specified types
*/
function withLogCollector<T extends readonly string[]>(
logsToCollect: T,
callback: () => void
): CollectedLogs<T>;
type LogFuncs = 'log' | 'warn' | 'error';
type CollectedLogs<T extends readonly string[]> = { [K in T[number]]: string[] };Usage Examples:
import { withLogCollector } from "@backstage/test-utils";
// Collect all console output types
const logs = await withLogCollector(async () => {
console.log('Info message');
console.warn('Warning message');
console.error('Error message');
// Execute component that produces logs
await someAsyncOperation();
});
expect(logs.log).toContain('Info message');
expect(logs.warn).toContain('Warning message');
expect(logs.error).toContain('Error message');
// Collect only specific log types
const errorLogs = await withLogCollector(['error'], async () => {
console.log('This will not be captured');
console.error('This will be captured');
render(<ComponentThatMightError />);
});
expect(errorLogs.error).toHaveLength(1);
expect(errorLogs.error[0]).toContain('This will be captured');
// Synchronous log collection
const syncLogs = withLogCollector(() => {
console.warn('Synchronous warning');
someFunction();
});
expect(syncLogs.warn).toContain('Synchronous warning');Renders React components with proper handling of async effects using React's act utility.
/**
* Renders components with async effects wrapped in act() utility
* @param nodes - React element to render
* @param options - Render options including legacy root support
* @returns Promise resolving to React Testing Library render result
*/
function renderWithEffects(
nodes: ReactElement,
options?: RenderOptions & LegacyRootOption
): Promise<RenderResult>;
interface LegacyRootOption {
/** Use React 17 legacy root API instead of React 18 createRoot */
legacyRoot?: boolean;
}
interface RenderOptions {
container?: HTMLElement;
baseElement?: HTMLElement;
wrapper?: React.ComponentType<{ children: React.ReactNode }>;
}Usage Examples:
import { renderWithEffects } from "@backstage/test-utils";
// Render component with async effects
const { getByText, findByText } = await renderWithEffects(
<ComponentWithAsyncEffects />
);
// Wait for async content to appear
expect(await findByText('Loaded data')).toBeInTheDocument();
// With wrapper component
const { container } = await renderWithEffects(
<MyComponent />,
{
wrapper: ({ children }) => (
<ThemeProvider theme={mockTheme}>
{children}
</ThemeProvider>
)
}
);
// With legacy root for React 17 compatibility
await renderWithEffects(<LegacyComponent />, { legacyRoot: true });Mocks window.matchMedia for Material-UI's useMediaQuery hook testing.
/**
* @deprecated Use @backstage/core-components/testUtils instead
* Mocks window.matchMedia for Material UI useMediaQuery testing
* @param options - Breakpoint configuration
* @param options.matches - Whether the media query should match
*/
function mockBreakpoint(options: { matches: boolean }): void;Usage Examples:
import { mockBreakpoint } from "@backstage/test-utils";
describe('ResponsiveComponent', () => {
test('shows mobile layout on small screens', () => {
mockBreakpoint({ matches: true }); // Simulate small screen
render(<ResponsiveComponent />);
expect(screen.getByTestId('mobile-layout')).toBeInTheDocument();
});
test('shows desktop layout on large screens', () => {
mockBreakpoint({ matches: false }); // Simulate large screen
render(<ResponsiveComponent />);
expect(screen.getByTestId('desktop-layout')).toBeInTheDocument();
});
afterEach(() => {
// Clean up mock
jest.restoreAllMocks();
});
});import { withLogCollector, renderInTestApp } from "@backstage/test-utils";
test('component logs expected messages during lifecycle', async () => {
const logs = await withLogCollector(['log', 'warn', 'error'], async () => {
const { rerender, unmount } = await renderInTestApp(<MyComponent />);
// Trigger state changes that might produce logs
fireEvent.click(screen.getByRole('button'));
// Update props
rerender(<MyComponent updated={true} />);
// Unmount component
unmount();
});
// Verify expected log messages
expect(logs.log).toContain('Component initialized');
expect(logs.warn).toContain('Deprecated prop used');
expect(logs.error).toHaveLength(0); // No errors expected
});import { withLogCollector, renderWithEffects } from "@backstage/test-utils";
function ErrorBoundary({ children }: { children: React.ReactNode }) {
// Error boundary implementation
}
test('error boundary catches and logs errors', async () => {
const logs = await withLogCollector(['error'], async () => {
await renderWithEffects(
<ErrorBoundary>
<ComponentThatThrows />
</ErrorBoundary>
);
});
expect(logs.error).toContain('Component error caught');
});import { withLogCollector } from "@backstage/test-utils";
test('component performance is within acceptable range', async () => {
const logs = await withLogCollector(['log'], async () => {
performance.mark('component-start');
await renderInTestApp(<PerformanceComponent />);
performance.mark('component-end');
performance.measure('component-render', 'component-start', 'component-end');
const measure = performance.getEntriesByName('component-render')[0];
console.log(`Render time: ${measure.duration}ms`);
});
const renderTimeLog = logs.log.find(log => log.includes('Render time:'));
expect(renderTimeLog).toBeDefined();
const duration = parseFloat(renderTimeLog!.match(/(\d+\.?\d*)ms/)?.[1] || '0');
expect(duration).toBeLessThan(100); // Less than 100ms
});type ReactElement = React.ReactElement;
interface RenderResult {
container: HTMLElement;
baseElement: HTMLElement;
debug: (baseElement?: HTMLElement | DocumentFragment) => void;
rerender: (ui: React.ReactElement) => void;
unmount: () => boolean;
asFragment: () => DocumentFragment;
// React Testing Library query methods
getByLabelText: (text: string | RegExp) => HTMLElement;
getByPlaceholderText: (text: string | RegExp) => HTMLElement;
getByText: (text: string | RegExp) => HTMLElement;
getByDisplayValue: (text: string | RegExp) => HTMLElement;
getByAltText: (text: string | RegExp) => HTMLElement;
getByTitle: (text: string | RegExp) => HTMLElement;
getByRole: (role: string, options?: any) => HTMLElement;
getByTestId: (testId: string) => HTMLElement;
// Async query methods
findByLabelText: (text: string | RegExp) => Promise<HTMLElement>;
findByPlaceholderText: (text: string | RegExp) => Promise<HTMLElement>;
findByText: (text: string | RegExp) => Promise<HTMLElement>;
findByDisplayValue: (text: string | RegExp) => Promise<HTMLElement>;
findByAltText: (text: string | RegExp) => Promise<HTMLElement>;
findByTitle: (text: string | RegExp) => Promise<HTMLElement>;
findByRole: (role: string, options?: any) => Promise<HTMLElement>;
findByTestId: (testId: string) => Promise<HTMLElement>;
// Multiple element queries
getAllByLabelText: (text: string | RegExp) => HTMLElement[];
getAllByPlaceholderText: (text: string | RegExp) => HTMLElement[];
getAllByText: (text: string | RegExp) => HTMLElement[];
getAllByDisplayValue: (text: string | RegExp) => HTMLElement[];
getAllByAltText: (text: string | RegExp) => HTMLElement[];
getAllByTitle: (text: string | RegExp) => HTMLElement[];
getAllByRole: (role: string, options?: any) => HTMLElement[];
getAllByTestId: (testId: string) => HTMLElement[];
// Optional query methods
queryByLabelText: (text: string | RegExp) => HTMLElement | null;
queryByPlaceholderText: (text: string | RegExp) => HTMLElement | null;
queryByText: (text: string | RegExp) => HTMLElement | null;
queryByDisplayValue: (text: string | RegExp) => HTMLElement | null;
queryByAltText: (text: string | RegExp) => HTMLElement | null;
queryByTitle: (text: string | RegExp) => HTMLElement | null;
queryByRole: (role: string, options?: any) => HTMLElement | null;
queryByTestId: (testId: string) => HTMLElement | null;
}
/**
* Type-safe log collection result
*/
type CollectedLogs<T extends readonly string[]> = {
[K in T[number]]: string[];
};
/**
* Default console log function types
*/
type LogFuncs = 'log' | 'warn' | 'error';