mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
This comprehensive architectural transformation removes all static theme imports (supersetTheme, themeObject) across the entire codebase, replacing them with proper dynamic theme access patterns that support real-time theme switching. ## What Changed **Static Exports Eliminated:** - Removed `supersetTheme` and `themeObject` exports from core theme module - Eliminated static theme dependencies across 47 files - Updated ESLint rules to reflect removed exports **Dynamic Theme Architecture:** - Functional components: Use `useTheme()` hook for reactive theme access - Class components: Use `withTheme()` HOC for theme injection - Transform functions: Access `theme` from chartProps parameter - Test infrastructure: Use `Theme.fromConfig()` for isolated testing - Singleton pattern: `DEFAULT_THEME` for efficient fallbacks **Test Architecture Cleanup:** - Removed unnecessary theme setup from 30+ test files - Eliminated legacy `dynamicTheme` cruft from logic tests - Simplified theme assertions to focus on behavior vs implementation details - Maintained theme testing only where legitimately needed **Core Infrastructure:** - ThemeController uses dynamic theme creation instead of static imports - ChartProps uses singleton DEFAULT_THEME for efficient fallbacks - Theme providers only at app root and isolated contexts (tests, storybook) ## Why This Was Needed The previous architecture had static theme imports that: - Always returned light theme values regardless of current theme mode - Broke dark mode compatibility in visualizations (fixed in previous commit) - Created performance overhead with redundant theme instance creation - Prevented real-time theme switching across components - Led to inconsistent theme access patterns ## Benefits - ✅ Perfect dark mode support - no static dependencies to break theming - ✅ True dynamic theming - all components react to theme changes - ✅ Clean architecture - minimal providers, consistent patterns - ✅ Better performance - singleton pattern eliminates waste - ✅ Future-proof - ready for theme customization and user preferences - ✅ Developer experience - clear patterns for every context This transformation enables the next generation of Superset theming with complete dynamic theme support and perfect dark mode compatibility. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
4.4 KiB
TypeScript
150 lines
4.4 KiB
TypeScript
/**
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you 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 '@testing-library/jest-dom';
|
|
import { ReactNode, ReactElement } from 'react';
|
|
// eslint-disable-next-line no-restricted-imports
|
|
import {
|
|
render,
|
|
RenderOptions,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import { Theme } from '@superset-ui/core';
|
|
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
|
import { ThemeController } from 'src/theme/ThemeController';
|
|
import { BrowserRouter } from 'react-router-dom';
|
|
import { Provider } from 'react-redux';
|
|
import { DndProvider } from 'react-dnd';
|
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
import reducerIndex from 'spec/helpers/reducerIndex';
|
|
import { QueryParamProvider } from 'use-query-params';
|
|
import { configureStore, Store } from '@reduxjs/toolkit';
|
|
import { api } from 'src/hooks/apiResources/queryApi';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
|
|
|
|
type Options = Omit<RenderOptions, 'queries'> & {
|
|
useRedux?: boolean;
|
|
useDnd?: boolean;
|
|
useQueryParams?: boolean;
|
|
useRouter?: boolean;
|
|
useTheme?: boolean;
|
|
initialState?: {};
|
|
reducers?: {};
|
|
store?: Store;
|
|
};
|
|
|
|
// Create theme controller for advanced testing scenarios
|
|
const themeController = new ThemeController();
|
|
|
|
export const createStore = (initialState: object = {}, reducers: object = {}) =>
|
|
configureStore({
|
|
preloadedState: initialState,
|
|
reducer: {
|
|
...reducers,
|
|
[api.reducerPath]: api.reducer,
|
|
},
|
|
middleware: getDefaultMiddleware =>
|
|
getDefaultMiddleware().concat(api.middleware),
|
|
devTools: false,
|
|
});
|
|
|
|
export const defaultStore = createStore();
|
|
|
|
export function createWrapper(options?: Options) {
|
|
const {
|
|
useDnd,
|
|
useRedux,
|
|
useQueryParams,
|
|
useRouter,
|
|
useTheme,
|
|
initialState,
|
|
reducers,
|
|
store,
|
|
} = options || {};
|
|
|
|
return ({ children }: { children?: ReactNode }) => {
|
|
// Create theme instance for this test session
|
|
const testTheme = Theme.fromConfig();
|
|
let result = (
|
|
<testTheme.SupersetThemeProvider>
|
|
<ExtensionsProvider>{children}</ExtensionsProvider>
|
|
</testTheme.SupersetThemeProvider>
|
|
);
|
|
|
|
if (useTheme) {
|
|
result = (
|
|
<SupersetThemeProvider themeController={themeController}>
|
|
{result}
|
|
</SupersetThemeProvider>
|
|
);
|
|
}
|
|
|
|
if (useDnd) {
|
|
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>;
|
|
}
|
|
|
|
if (useRedux || store) {
|
|
const mockStore =
|
|
store ?? createStore(initialState, reducers || reducerIndex);
|
|
result = <Provider store={mockStore}>{result}</Provider>;
|
|
}
|
|
|
|
if (useQueryParams) {
|
|
result = <QueryParamProvider>{result}</QueryParamProvider>;
|
|
}
|
|
|
|
if (useRouter) {
|
|
result = <BrowserRouter>{result}</BrowserRouter>;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
const customRender = (ui: ReactElement, options?: Options) =>
|
|
render(ui, { wrapper: createWrapper(options), ...options });
|
|
|
|
export function sleep(time: number) {
|
|
return new Promise(resolve => {
|
|
setTimeout(resolve, time);
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-imports
|
|
export * from '@testing-library/react';
|
|
export { customRender as render };
|
|
export { default as userEvent } from '@testing-library/user-event';
|
|
|
|
export async function selectOption(option: string, selectName?: string) {
|
|
const select = screen.getByRole(
|
|
'combobox',
|
|
selectName ? { name: selectName } : {},
|
|
);
|
|
await userEvent.click(select);
|
|
const item = await waitFor(() =>
|
|
within(
|
|
// eslint-disable-next-line testing-library/no-node-access
|
|
document.querySelector('.rc-virtual-list')!,
|
|
).getByText(option),
|
|
);
|
|
await userEvent.click(item);
|
|
}
|