codeant-ai-for-open-source[bot] commented on code in PR #37378:
URL: https://github.com/apache/superset/pull/37378#discussion_r2755623008
##########
superset-frontend/src/theme/ThemeController.ts:
##########
@@ -796,10 +821,25 @@ export class ThemeController {
this.loadFonts(fontUrls);
} catch (error) {
console.error('Failed to apply theme:', error);
- this.fallbackToDefaultMode();
+ // Re-throw the error so updateTheme can handle fallback logic
+ throw error;
}
}
+ private async applyThemeWithRecovery(theme: AnyThemeConfig): Promise<void> {
+ // Note: This method re-throws errors to the caller instead of calling
+ // fallbackToDefaultMode directly, to avoid infinite recursion since
+ // fallbackToDefaultMode calls this method. The caller's try/catch
+ // handles the fallback flow.
+ const normalizedConfig = normalizeThemeConfig(theme);
+ this.globalTheme.setConfig(normalizedConfig);
+
+ // Load custom fonts if specified, mirroring applyTheme() behavior
+ const fontUrls = (normalizedConfig?.token as Record<string, unknown>)
+ ?.fontUrls as string[] | undefined;
+ this.loadFonts(fontUrls);
Review Comment:
**Suggestion:** When applying the fetched system default theme in
applyThemeWithRecovery, any error thrown by normalizeThemeConfig or setConfig
is caught by fallbackToDefaultMode and silently swallowed, so the failure is
completely invisible in logs and very hard to diagnose; wrapping this method in
a try/catch that logs before rethrowing preserves the error information while
keeping the existing fallback behavior. [possible bug]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Recovery failures produce limited diagnostic logs.
- ⚠️ Theme recovery debugging is harder for engineers.
- ⚠️ Runtime theme fallback may proceed silently.
```
</details>
```suggestion
try {
const normalizedConfig = normalizeThemeConfig(theme);
this.globalTheme.setConfig(normalizedConfig);
// Load custom fonts if specified, mirroring applyTheme() behavior
const fontUrls = (normalizedConfig?.token as Record<string, unknown>)
?.fontUrls as string[] | undefined;
this.loadFonts(fontUrls);
} catch (error) {
console.error(
'Failed to apply system default theme during recovery:',
error,
);
throw error;
}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open superset-frontend/src/theme/ThemeController.ts and locate
applyThemeWithRecovery
at lines 829-841. Note this method is invoked by fallbackToDefaultMode (see
fallbackToDefaultMode where it calls await
this.applyThemeWithRecovery(freshSystemTheme)).
2. Arrange for fallbackToDefaultMode to run in the code path: for example,
trigger
updateTheme to throw by having an invalid theme applied (updateTheme's
try/catch will call
fallbackToDefaultMode). The call chain is updateTheme ->
fallbackToDefaultMode ->
fetchSystemDefaultTheme -> applyThemeWithRecovery (applyThemeWithRecovery is
defined at
superset-frontend/src/theme/ThemeController.ts:829-841).
3. Cause normalizeThemeConfig or this.globalTheme.setConfig to throw (e.g.,
by returning a
fetched system theme JSON with malformed structure from
fetchSystemDefaultTheme). When
applyThemeWithRecovery throws, current implementation re-throws to the
caller without
logging inside this method.
4. Observe behavior: fallbackToDefaultMode calls applyThemeWithRecovery and
catches errors
from the attempt (fallbackToDefaultMode silently proceeds to other
fallbacks). Because
applyThemeWithRecovery contains no internal logging, the original error
context is not
logged at the point of failure (only higher-level code paths log generic
messages). Adding
a try/catch with console.error in applyThemeWithRecovery would surface the
failure with
full stack and message before rethrowing.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/ThemeController.ts
**Line:** 834:840
**Comment:**
*Possible Bug: When applying the fetched system default theme in
applyThemeWithRecovery, any error thrown by normalizeThemeConfig or setConfig
is caught by fallbackToDefaultMode and silently swallowed, so the failure is
completely invisible in logs and very hard to diagnose; wrapping this method in
a try/catch that logs before rethrowing preserves the error information while
keeping the existing fallback behavior.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/theme/tests/ThemeController.test.ts:
##########
@@ -833,6 +833,191 @@ test('ThemeController handles theme application errors',
() => {
fallbackSpy.mockRestore();
});
+test('ThemeController constructor recovers from corrupted stored theme', () =>
{
+ // Simulate corrupted dev theme override in storage
+ const corruptedTheme = { token: { colorPrimary: '#ff0000' } };
+ mockLocalStorage.getItem.mockImplementation((key: string) => {
+ if (key === 'superset-dev-theme-override') {
+ return JSON.stringify(corruptedTheme);
+ }
+ return null;
+ });
+
+ // Mock Theme.fromConfig to return object with toSerializedConfig
+ mockThemeFromConfig.mockReturnValue({
+ ...mockThemeObject,
+ toSerializedConfig: () => corruptedTheme,
+ });
+
+ // First call throws (corrupted theme), second call succeeds (fallback)
+ let callCount = 0;
+ mockSetConfig.mockImplementation(() => {
+ callCount += 1;
+ if (callCount === 1) {
+ throw new Error('Invalid theme configuration');
+ }
+ });
+
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
+
+ // Should not throw - constructor should recover
+ const controller = createController();
+
+ // Verify recovery happened
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ 'Failed to apply stored theme, clearing invalid overrides:',
+ expect.any(Error),
+ );
+
+ // Verify invalid overrides were cleared from storage
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-dev-theme-override',
+ );
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-crud-theme-id',
+ );
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-applied-theme-id',
+ );
+
+ // Verify controller is in a valid state
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+
+ consoleWarnSpy.mockRestore();
+});
+
+test('recovery flow: fetchSystemDefaultTheme returns theme → applies fetched
theme', async () => {
+ // Test: fallbackToDefaultMode fetches theme from API and applies it
+ // Flow: fallbackToDefaultMode → fetchSystemDefaultTheme →
applyThemeWithRecovery
+
+ const controller = createController();
+
+ // Mock fetch to return a system default theme from API
+ const systemTheme = { token: { colorPrimary: '#recovery-theme' } };
+ const mockFetch = jest.fn().mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ result: [{ json_data: JSON.stringify(systemTheme) }],
+ }),
+ });
+ global.fetch = mockFetch;
+
+ // Track setConfig calls to verify the fetched theme is applied
+ const setConfigCalls: unknown[] = [];
+ mockSetConfig.mockImplementation((config: unknown) => {
+ setConfigCalls.push(config);
+ });
+
+ // Trigger fallbackToDefaultMode (simulates what happens after applyTheme
fails)
+ await (controller as any).fallbackToDefaultMode();
+
+ // Verify API was called to fetch system default theme
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining('/api/v1/theme/'),
+ );
+
+ // Verify the fetched theme was applied via applyThemeWithRecovery
+ expect(setConfigCalls.length).toBe(1);
+ expect(setConfigCalls[0]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({ colorPrimary: '#recovery-theme' }),
+ }),
+ );
+
+ // Verify controller is in default mode
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
Review Comment:
**Suggestion:** This test overrides the global `fetch` function and never
restores it, so later tests in the same Jest environment will keep using this
mock instead of their own, leading to hidden coupling and potentially incorrect
behavior in unrelated tests. [resource leak]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ Jest tests using real fetch may fail unpredictably.
- ⚠️ Theme recovery tests can become flaky across runs.
- ⚠️ Other test files in same worker may see mocked fetch.
```
</details>
```suggestion
const originalFetch = global.fetch;
global.fetch = mockFetch;
try {
// Track setConfig calls to verify the fetched theme is applied
const setConfigCalls: unknown[] = [];
mockSetConfig.mockImplementation((config: unknown) => {
setConfigCalls.push(config);
});
// Trigger fallbackToDefaultMode (simulates what happens after
applyTheme fails)
await (controller as any).fallbackToDefaultMode();
// Verify API was called to fetch system default theme
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/v1/theme/'),
);
// Verify the fetched theme was applied via applyThemeWithRecovery
expect(setConfigCalls.length).toBe(1);
expect(setConfigCalls[0]).toEqual(
expect.objectContaining({
token: expect.objectContaining({ colorPrimary: '#recovery-theme' }),
}),
);
// Verify controller is in default mode
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
} finally {
global.fetch = originalFetch;
}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Run Jest for this test file
(superset-frontend/src/theme/tests/ThemeController.test.ts).
2. Test execution reaches the assignment at
src/theme/tests/ThemeController.test.ts:903
where the code sets global.fetch = mockFetch (the mock is created at lines
~895-901 and
assigned at ~903 in the same test).
3. The test does not restore global.fetch after completion (no
cleanup/restore present in
this test), so the mocked fetch remains on the Node global object for the
remainder of the
Jest process.
4. Subsequent tests running in the same Jest worker (either later tests in
this file or
other test files executed in the same process) will observe the mocked
global.fetch
instead of the original implementation, producing unexpected behavior or
false
positives/failures. The lack of restoration is verifiable by running the
file with
--runInBand and observing global.fetch remains the jest.fn after this test
finishes.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/tests/ThemeController.test.ts
**Line:** 903:928
**Comment:**
*Resource Leak: This test overrides the global `fetch` function and
never restores it, so later tests in the same Jest environment will keep using
this mock instead of their own, leading to hidden coupling and potentially
incorrect behavior in unrelated tests.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/theme/tests/ThemeController.test.ts:
##########
@@ -833,6 +833,191 @@ test('ThemeController handles theme application errors',
() => {
fallbackSpy.mockRestore();
});
+test('ThemeController constructor recovers from corrupted stored theme', () =>
{
+ // Simulate corrupted dev theme override in storage
+ const corruptedTheme = { token: { colorPrimary: '#ff0000' } };
+ mockLocalStorage.getItem.mockImplementation((key: string) => {
+ if (key === 'superset-dev-theme-override') {
+ return JSON.stringify(corruptedTheme);
+ }
+ return null;
+ });
+
+ // Mock Theme.fromConfig to return object with toSerializedConfig
+ mockThemeFromConfig.mockReturnValue({
+ ...mockThemeObject,
+ toSerializedConfig: () => corruptedTheme,
+ });
+
+ // First call throws (corrupted theme), second call succeeds (fallback)
+ let callCount = 0;
+ mockSetConfig.mockImplementation(() => {
+ callCount += 1;
+ if (callCount === 1) {
+ throw new Error('Invalid theme configuration');
+ }
+ });
+
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
+
+ // Should not throw - constructor should recover
+ const controller = createController();
+
+ // Verify recovery happened
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ 'Failed to apply stored theme, clearing invalid overrides:',
+ expect.any(Error),
+ );
+
+ // Verify invalid overrides were cleared from storage
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-dev-theme-override',
+ );
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-crud-theme-id',
+ );
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-applied-theme-id',
+ );
+
+ // Verify controller is in a valid state
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+
+ consoleWarnSpy.mockRestore();
+});
+
+test('recovery flow: fetchSystemDefaultTheme returns theme → applies fetched
theme', async () => {
+ // Test: fallbackToDefaultMode fetches theme from API and applies it
+ // Flow: fallbackToDefaultMode → fetchSystemDefaultTheme →
applyThemeWithRecovery
+
+ const controller = createController();
+
+ // Mock fetch to return a system default theme from API
+ const systemTheme = { token: { colorPrimary: '#recovery-theme' } };
+ const mockFetch = jest.fn().mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ result: [{ json_data: JSON.stringify(systemTheme) }],
+ }),
+ });
+ global.fetch = mockFetch;
+
+ // Track setConfig calls to verify the fetched theme is applied
+ const setConfigCalls: unknown[] = [];
+ mockSetConfig.mockImplementation((config: unknown) => {
+ setConfigCalls.push(config);
+ });
+
+ // Trigger fallbackToDefaultMode (simulates what happens after applyTheme
fails)
+ await (controller as any).fallbackToDefaultMode();
+
+ // Verify API was called to fetch system default theme
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining('/api/v1/theme/'),
+ );
+
+ // Verify the fetched theme was applied via applyThemeWithRecovery
+ expect(setConfigCalls.length).toBe(1);
+ expect(setConfigCalls[0]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({ colorPrimary: '#recovery-theme' }),
+ }),
+ );
+
+ // Verify controller is in default mode
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+});
+
+test('recovery flow: both API fetches fail → falls back to cached default
theme', async () => {
+ // Test: When fetchSystemDefaultTheme fails, fallbackToDefaultMode uses
cached theme
+ // Flow: fallbackToDefaultMode → fetchSystemDefaultTheme (fails) →
applyTheme(cached)
+
+ const controller = createController();
+
+ // Mock fetch to fail for both API endpoints
+ const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'));
+ global.fetch = mockFetch;
+
+ // Track setConfig calls
+ const setConfigCalls: unknown[] = [];
+ mockSetConfig.mockImplementation((config: unknown) => {
+ setConfigCalls.push(config);
+ });
+
+ // Trigger fallbackToDefaultMode
+ await (controller as any).fallbackToDefaultMode();
+
+ // Verify fetch was attempted
+ expect(mockFetch).toHaveBeenCalled();
+
+ // Verify fallback to cached default theme was applied via applyTheme
+ expect(setConfigCalls.length).toBe(1);
+ expect(setConfigCalls[0]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({
+ colorBgBase: '#ededed', // From DEFAULT_THEME in test setup
+ }),
+ }),
+ );
+
+ // Verify controller is in default mode
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+});
+
+test('recovery flow: fetched theme fails to apply → falls back to cached
default', async () => {
+ // Test: When applyThemeWithRecovery fails, fallbackToDefaultMode uses
cached theme
+ // Flow: fallbackToDefaultMode → fetchSystemDefaultTheme →
applyThemeWithRecovery (fails) → applyTheme(cached)
+
+ const controller = createController();
+
+ // Mock fetch to return a theme
+ const systemTheme = { token: { colorPrimary: '#bad-theme' } };
+ const mockFetch = jest.fn().mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ result: [{ json_data: JSON.stringify(systemTheme) }],
+ }),
+ });
+ global.fetch = mockFetch;
+
+ // First setConfig call (applyThemeWithRecovery) fails, second (applyTheme)
succeeds
+ const setConfigCalls: unknown[] = [];
+ mockSetConfig.mockImplementation((config: unknown) => {
+ setConfigCalls.push(config);
+ if (setConfigCalls.length === 1) {
+ throw new Error('Fetched theme failed to apply');
+ }
+ });
+
+ // Trigger fallbackToDefaultMode
+ await (controller as any).fallbackToDefaultMode();
+
+ // Verify fetch was called
+ expect(mockFetch).toHaveBeenCalled();
+
+ // Verify both attempts were made: fetched theme (failed) then cached default
+ expect(setConfigCalls.length).toBe(2);
+
+ // First call was the fetched theme (which failed)
+ expect(setConfigCalls[0]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({ colorPrimary: '#bad-theme' }),
+ }),
+ );
+
+ // Second call was the cached default theme
+ expect(setConfigCalls[1]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({
+ colorBgBase: '#ededed', // From DEFAULT_THEME
+ }),
+ }),
+ );
+
+ // Verify controller is in default mode
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
Review Comment:
**Suggestion:** Similar to the other recovery tests, this one overwrites
`global.fetch` with a mock and never restores it, so the mock can leak into
other tests and cause them to see the wrong fetch behavior. [resource leak]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ Unrelated tests that call fetch fail intermittently.
- ⚠️ CI test runs may become flaky and unreliable.
- ⚠️ Debugging intermittent test failures increases.
```
</details>
```suggestion
const originalFetch = global.fetch;
global.fetch = mockFetch;
try {
// First setConfig call (applyThemeWithRecovery) fails, second
(applyTheme) succeeds
const setConfigCalls: unknown[] = [];
mockSetConfig.mockImplementation((config: unknown) => {
setConfigCalls.push(config);
if (setConfigCalls.length === 1) {
throw new Error('Fetched theme failed to apply');
}
});
// Trigger fallbackToDefaultMode
await (controller as any).fallbackToDefaultMode();
// Verify fetch was called
expect(mockFetch).toHaveBeenCalled();
// Verify both attempts were made: fetched theme (failed) then cached
default
expect(setConfigCalls.length).toBe(2);
// First call was the fetched theme (which failed)
expect(setConfigCalls[0]).toEqual(
expect.objectContaining({
token: expect.objectContaining({ colorPrimary: '#bad-theme' }),
}),
);
// Second call was the cached default theme
expect(setConfigCalls[1]).toEqual(
expect.objectContaining({
token: expect.objectContaining({
colorBgBase: '#ededed', // From DEFAULT_THEME
}),
}),
);
// Verify controller is in default mode
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
} finally {
global.fetch = originalFetch;
}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Execute the test file
superset-frontend/src/theme/tests/ThemeController.test.ts under
Jest.
2. The test constructs a mock fetch and assigns it to the global at
src/theme/tests/ThemeController.test.ts:981 (mock defined ~977, assigned at
~981).
3. This test does not restore the original global.fetch on completion, so
the jest.fn
remains present on the global object.
4. Any subsequent test in the same Jest worker that expects either a
different fetch mock
or the original fetch implementation will run against this leftover mock,
causing
unexpected invocations or assertion mismatches; running the file with
--runInBand will
make this leak observable.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/tests/ThemeController.test.ts
**Line:** 981:1018
**Comment:**
*Resource Leak: Similar to the other recovery tests, this one
overwrites `global.fetch` with a mock and never restores it, so the mock can
leak into other tests and cause them to see the wrong fetch behavior.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/theme/utils/antdTokenNames.test.ts:
##########
@@ -0,0 +1,108 @@
+/**
+ * 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 {
+ isValidTokenName,
+ isSupersetCustomToken,
+ getAllValidTokenNames,
+} from './antdTokenNames';
+
+test('isValidTokenName recognizes standard Ant Design tokens', () => {
+ expect(isValidTokenName('colorPrimary')).toBe(true);
+ expect(isValidTokenName('fontSize')).toBe(true);
+ expect(isValidTokenName('padding')).toBe(true);
+ expect(isValidTokenName('borderRadius')).toBe(true);
+});
+
+test('isValidTokenName recognizes Superset custom tokens', () => {
+ expect(isValidTokenName('brandLogoUrl')).toBe(true);
+ expect(isValidTokenName('brandSpinnerSvg')).toBe(true);
+ expect(isValidTokenName('fontSizeXS')).toBe(true);
+ expect(isValidTokenName('echartsOptionsOverrides')).toBe(true);
+});
+
+test('isValidTokenName rejects unknown tokens', () => {
+ expect(isValidTokenName('fooBarBaz')).toBe(false);
+ expect(isValidTokenName('colrPrimary')).toBe(false);
+ expect(isValidTokenName('invalidToken')).toBe(false);
+});
+
+test('isValidTokenName handles edge cases', () => {
+ expect(isValidTokenName('')).toBe(false);
+ expect(isValidTokenName(' ')).toBe(false);
+});
+
+test('isSupersetCustomToken identifies Superset-specific tokens', () => {
+ expect(isSupersetCustomToken('brandLogoUrl')).toBe(true);
+ expect(isSupersetCustomToken('brandSpinnerSvg')).toBe(true);
+ expect(isSupersetCustomToken('fontSizeXS')).toBe(true);
+ expect(isSupersetCustomToken('fontUrls')).toBe(true);
Review Comment:
**Suggestion:** The test asserts that a Superset-specific token named
"fontUrls" is recognized by the custom token detection, but the current
implementation of the token registry does not define "fontUrls" as a Superset
custom token, so this expectation will fail and incorrectly signal a regression
in the implementation. [logic error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Unit test failure blocks CI checks.
- ⚠️ Theme token utilities tests unreliable.
- ⚠️ Developers hit failing tests locally.
```
</details>
```suggestion
expect(isSupersetCustomToken('echartsOptionsOverrides')).toBe(true);
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Run the unit tests for the theme utilities: from repository root run
`yarn test
src/theme/utils/antdTokenNames.test.ts`. This executes the test file
`superset-frontend/src/theme/utils/antdTokenNames.test.ts` which contains
the failing
case.
2. The test named "isSupersetCustomToken identifies Superset-specific
tokens" (located at
superset-frontend/src/theme/utils/antdTokenNames.test.ts:50-55) performs the
assertion at
line 54: `expect(isSupersetCustomToken('fontUrls')).toBe(true);`.
3. The test calls isSupersetCustomToken implemented at
superset-frontend/src/theme/utils/antdTokenNames.ts:92-94 which returns
`SUPERSET_CUSTOM_TOKENS.has(tokenName)`. If `SUPERSET_CUSTOM_TOKENS` does
not include
'fontUrls', the call returns false and the assertion at line 54 fails with
Jest output
like "Expected false to be true".
4. To confirm, open superset-frontend/src/theme/utils/antdTokenNames.ts and
inspect the
`SUPERSET_CUSTOM_TOKENS` definition (search for the constant in that file).
If 'fontUrls'
is absent, the test expectation is incorrect and should be replaced with a
known token
(e.g., 'echartsOptionsOverrides') or the implementation updated to include
'fontUrls'.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/utils/antdTokenNames.test.ts
**Line:** 54:54
**Comment:**
*Logic Error: The test asserts that a Superset-specific token named
"fontUrls" is recognized by the custom token detection, but the current
implementation of the token registry does not define "fontUrls" as a Superset
custom token, so this expectation will fail and incorrectly signal a regression
in the implementation.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/theme/tests/ThemeController.test.ts:
##########
@@ -833,6 +833,191 @@ test('ThemeController handles theme application errors',
() => {
fallbackSpy.mockRestore();
});
+test('ThemeController constructor recovers from corrupted stored theme', () =>
{
+ // Simulate corrupted dev theme override in storage
+ const corruptedTheme = { token: { colorPrimary: '#ff0000' } };
+ mockLocalStorage.getItem.mockImplementation((key: string) => {
+ if (key === 'superset-dev-theme-override') {
+ return JSON.stringify(corruptedTheme);
+ }
+ return null;
+ });
+
+ // Mock Theme.fromConfig to return object with toSerializedConfig
+ mockThemeFromConfig.mockReturnValue({
+ ...mockThemeObject,
+ toSerializedConfig: () => corruptedTheme,
+ });
+
+ // First call throws (corrupted theme), second call succeeds (fallback)
+ let callCount = 0;
+ mockSetConfig.mockImplementation(() => {
+ callCount += 1;
+ if (callCount === 1) {
+ throw new Error('Invalid theme configuration');
+ }
+ });
+
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
+
+ // Should not throw - constructor should recover
+ const controller = createController();
+
+ // Verify recovery happened
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ 'Failed to apply stored theme, clearing invalid overrides:',
+ expect.any(Error),
+ );
+
+ // Verify invalid overrides were cleared from storage
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-dev-theme-override',
+ );
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-crud-theme-id',
+ );
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
+ 'superset-applied-theme-id',
+ );
+
+ // Verify controller is in a valid state
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+
+ consoleWarnSpy.mockRestore();
+});
+
+test('recovery flow: fetchSystemDefaultTheme returns theme → applies fetched
theme', async () => {
+ // Test: fallbackToDefaultMode fetches theme from API and applies it
+ // Flow: fallbackToDefaultMode → fetchSystemDefaultTheme →
applyThemeWithRecovery
+
+ const controller = createController();
+
+ // Mock fetch to return a system default theme from API
+ const systemTheme = { token: { colorPrimary: '#recovery-theme' } };
+ const mockFetch = jest.fn().mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ result: [{ json_data: JSON.stringify(systemTheme) }],
+ }),
+ });
+ global.fetch = mockFetch;
+
+ // Track setConfig calls to verify the fetched theme is applied
+ const setConfigCalls: unknown[] = [];
+ mockSetConfig.mockImplementation((config: unknown) => {
+ setConfigCalls.push(config);
+ });
+
+ // Trigger fallbackToDefaultMode (simulates what happens after applyTheme
fails)
+ await (controller as any).fallbackToDefaultMode();
+
+ // Verify API was called to fetch system default theme
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining('/api/v1/theme/'),
+ );
+
+ // Verify the fetched theme was applied via applyThemeWithRecovery
+ expect(setConfigCalls.length).toBe(1);
+ expect(setConfigCalls[0]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({ colorPrimary: '#recovery-theme' }),
+ }),
+ );
+
+ // Verify controller is in default mode
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+});
+
+test('recovery flow: both API fetches fail → falls back to cached default
theme', async () => {
+ // Test: When fetchSystemDefaultTheme fails, fallbackToDefaultMode uses
cached theme
+ // Flow: fallbackToDefaultMode → fetchSystemDefaultTheme (fails) →
applyTheme(cached)
+
+ const controller = createController();
+
+ // Mock fetch to fail for both API endpoints
+ const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'));
+ global.fetch = mockFetch;
+
+ // Track setConfig calls
+ const setConfigCalls: unknown[] = [];
+ mockSetConfig.mockImplementation((config: unknown) => {
+ setConfigCalls.push(config);
+ });
+
+ // Trigger fallbackToDefaultMode
+ await (controller as any).fallbackToDefaultMode();
+
+ // Verify fetch was attempted
+ expect(mockFetch).toHaveBeenCalled();
+
+ // Verify fallback to cached default theme was applied via applyTheme
+ expect(setConfigCalls.length).toBe(1);
+ expect(setConfigCalls[0]).toEqual(
+ expect.objectContaining({
+ token: expect.objectContaining({
+ colorBgBase: '#ededed', // From DEFAULT_THEME in test setup
+ }),
+ }),
+ );
+
+ // Verify controller is in default mode
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
Review Comment:
**Suggestion:** This test also assigns a mock to the global `fetch` function
without restoring the original, so the mocked implementation can persist past
the test boundary and interfere with other tests' use of `fetch`. [resource
leak]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ Downstream tests using network fetch fail unpredictably.
- ⚠️ Test suite runs become flaky locally and CI.
- ⚠️ Debug time increases for intermittent failures.
```
</details>
```suggestion
const originalFetch = global.fetch;
global.fetch = mockFetch;
try {
// Track setConfig calls
const setConfigCalls: unknown[] = [];
mockSetConfig.mockImplementation((config: unknown) => {
setConfigCalls.push(config);
});
// Trigger fallbackToDefaultMode
await (controller as any).fallbackToDefaultMode();
// Verify fetch was attempted
expect(mockFetch).toHaveBeenCalled();
// Verify fallback to cached default theme was applied via applyTheme
expect(setConfigCalls.length).toBe(1);
expect(setConfigCalls[0]).toEqual(
expect.objectContaining({
token: expect.objectContaining({
colorBgBase: '#ededed', // From DEFAULT_THEME in test setup
}),
}),
);
// Verify controller is in default mode
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
} finally {
global.fetch = originalFetch;
}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Run the test file
superset-frontend/src/theme/tests/ThemeController.test.ts with Jest.
2. Execution reaches the failing-fetch test where at
src/theme/tests/ThemeController.test.ts:939 the code assigns global.fetch =
mockFetch
(mock defined at ~937).
3. The test finishes without restoring global.fetch (no teardown in this
test), leaving
the rejected mock on the global object.
4. Other tests executed later in the same Jest worker will encounter the
rejected mock
fetch (observable by inspecting global.fetch after test completion), causing
unrelated
tests that expect network calls or different fetch behavior to fail or
behave incorrectly.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/tests/ThemeController.test.ts
**Line:** 939:964
**Comment:**
*Resource Leak: This test also assigns a mock to the global `fetch`
function without restoring the original, so the mocked implementation can
persist past the test boundary and interfere with other tests' use of `fetch`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/theme/ThemeController.ts:
##########
@@ -143,8 +143,26 @@ export class ThemeController {
// Setup change callback
if (onChange) this.onChangeCallbacks.add(onChange);
- // Apply initial theme and persist mode
- this.applyTheme(initialTheme);
+ // Apply initial theme with recovery for corrupted stored themes
+ try {
+ this.applyTheme(initialTheme);
+ } catch (error) {
+ // Corrupted dev override or CRUD theme in storage - clear and retry
with defaults
+ console.warn(
+ 'Failed to apply stored theme, clearing invalid overrides:',
+ error,
+ );
+ this.devThemeOverride = null;
+ this.crudThemeId = null;
+ this.storage.removeItem(STORAGE_KEYS.DEV_THEME_OVERRIDE);
+ this.storage.removeItem(STORAGE_KEYS.CRUD_THEME_ID);
+ this.storage.removeItem(STORAGE_KEYS.APPLIED_THEME_ID);
+
+ // Retry with clean default theme
+ this.currentMode = ThemeMode.DEFAULT;
+ const safeTheme = this.defaultTheme || {};
+ this.applyTheme(safeTheme);
+ }
this.persistMode();
}
Review Comment:
**Suggestion:** The initial theme application in the constructor directly
calls the low-level applyTheme method and never notifies registered listeners,
so any consumer relying on the onChange callback will not be informed of the
initial theme and can render using stale theme state until a later explicit
update happens. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Components subscribing via onChange won't receive initial theme.
- ⚠️ UI consumers may render with stale theme on first paint.
- ⚠️ Theme-based initialization code may run with wrong tokens.
```
</details>
```suggestion
this.notifyListeners();
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open the file superset-frontend/src/theme/ThemeController.ts and locate
the constructor
block at lines 143-166 (constructor initialization and initial theme
application). Observe
these lines add an onChange callback and call this.applyTheme(initialTheme)
without
calling notifyListeners().
2. In a running application (or a unit test), construct a ThemeController
instance with an
onChange callback: new ThemeController({ onChange: myCallback }) from
anywhere that
instantiates ThemeController (constructor code is defined at
superset-frontend/src/theme/ThemeController.ts:143-166). The callback is
added at the code
shown in those lines.
3. After the constructor finishes, verify myCallback was not invoked. The
code path in the
constructor (lines 143-166) does not call this.notifyListeners(), so
listeners registered
via the constructor option receive no initial notification.
4. Confirm that later theme changes do call listeners by triggering an
explicit theme
update (for example, call setTemporaryTheme or setTheme which eventually call
updateTheme). You will observe notifyListeners is called from updateTheme,
but the initial
registration in the constructor never produced an initial notification
(constructor block
at superset-frontend/src/theme/ThemeController.ts:143-166). This proves the
missing
notifyListeners() in the constructor prevents onChange consumers from
receiving the
initial theme.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/ThemeController.ts
**Line:** 167:167
**Comment:**
*Logic Error: The initial theme application in the constructor directly
calls the low-level applyTheme method and never notifies registered listeners,
so any consumer relying on the onChange callback will not be informed of the
initial theme and can render using stale theme state until a later explicit
update happens.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
##########
superset-frontend/src/theme/utils/antdTokenNames.test.ts:
##########
@@ -0,0 +1,108 @@
+/**
+ * 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 {
+ isValidTokenName,
+ isSupersetCustomToken,
+ getAllValidTokenNames,
+} from './antdTokenNames';
+
+test('isValidTokenName recognizes standard Ant Design tokens', () => {
+ expect(isValidTokenName('colorPrimary')).toBe(true);
+ expect(isValidTokenName('fontSize')).toBe(true);
+ expect(isValidTokenName('padding')).toBe(true);
+ expect(isValidTokenName('borderRadius')).toBe(true);
+});
+
+test('isValidTokenName recognizes Superset custom tokens', () => {
+ expect(isValidTokenName('brandLogoUrl')).toBe(true);
+ expect(isValidTokenName('brandSpinnerSvg')).toBe(true);
+ expect(isValidTokenName('fontSizeXS')).toBe(true);
+ expect(isValidTokenName('echartsOptionsOverrides')).toBe(true);
+});
+
+test('isValidTokenName rejects unknown tokens', () => {
+ expect(isValidTokenName('fooBarBaz')).toBe(false);
+ expect(isValidTokenName('colrPrimary')).toBe(false);
+ expect(isValidTokenName('invalidToken')).toBe(false);
+});
+
+test('isValidTokenName handles edge cases', () => {
+ expect(isValidTokenName('')).toBe(false);
+ expect(isValidTokenName(' ')).toBe(false);
+});
+
+test('isSupersetCustomToken identifies Superset-specific tokens', () => {
+ expect(isSupersetCustomToken('brandLogoUrl')).toBe(true);
+ expect(isSupersetCustomToken('brandSpinnerSvg')).toBe(true);
+ expect(isSupersetCustomToken('fontSizeXS')).toBe(true);
+ expect(isSupersetCustomToken('fontUrls')).toBe(true);
+});
+
+test('isSupersetCustomToken returns false for Ant Design tokens', () => {
+ expect(isSupersetCustomToken('colorPrimary')).toBe(false);
+ expect(isSupersetCustomToken('fontSize')).toBe(false);
+});
+
+test('isSupersetCustomToken returns false for unknown tokens', () => {
+ expect(isSupersetCustomToken('fooBar')).toBe(false);
+});
+
+test('getAllValidTokenNames returns categorized token names', () => {
+ const result = getAllValidTokenNames();
+
+ expect(result).toHaveProperty('antdTokens');
+ expect(result).toHaveProperty('supersetTokens');
+ expect(result).toHaveProperty('total');
+});
+
+test('getAllValidTokenNames has reasonable token counts', () => {
+ const result = getAllValidTokenNames();
+
+ // Ant Design tokens should exist (avoid brittle exact count that breaks on
upgrades)
+ expect(result.antdTokens.length).toBeGreaterThan(0);
+ expect(result.antdTokens).toContain('colorPrimary');
+ expect(result.antdTokens).toContain('fontSize');
+ expect(result.antdTokens).toContain('borderRadius');
+
+ // Superset custom tokens should exist
+ expect(result.supersetTokens.length).toBeGreaterThan(0);
+ expect(result.supersetTokens).toContain('brandLogoUrl');
+ expect(result.supersetTokens).toContain('fontUrls');
Review Comment:
**Suggestion:** The test for `getAllValidTokenNames` assumes that the
Superset token list contains "fontUrls", but since this token is not actually
part of the `SUPERSET_CUSTOM_TOKENS` set in the implementation, the assertion
will fail, making the test suite inconsistent with real behavior. [logic error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Unit test failure blocks CI checks.
- ⚠️ Theme token list validation tests unreliable.
- ⚠️ PRs modifying tokens risk false negatives.
```
</details>
```suggestion
expect(result.supersetTokens).toContain('echartsOptionsOverrides');
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Run the specific test block: `yarn test
src/theme/utils/antdTokenNames.test.ts` which
runs the "getAllValidTokenNames has reasonable token counts" test (file
lines ~74-92).
2. The test creates `result = getAllValidTokenNames()` and asserts at line
86:
`expect(result.supersetTokens).toContain('fontUrls');`.
3. `getAllValidTokenNames()` is implemented at
superset-frontend/src/theme/utils/antdTokenNames.ts:100-116; it sets
`supersetTokens =
Array.from(SUPERSET_CUSTOM_TOKENS)`. If `SUPERSET_CUSTOM_TOKENS` does not
include
'fontUrls', `result.supersetTokens` will not contain it and the assertion at
line 86 fails
with Jest output like "Expected array to contain 'fontUrls'".
4. Verify the token list by inspecting the `SUPERSET_CUSTOM_TOKENS`
definition in
superset-frontend/src/theme/utils/antdTokenNames.ts. If 'fontUrls' is not
present the test
should be updated to assert an actual token (for example
'echartsOptionsOverrides') or the
token set should be intentionally extended.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset-frontend/src/theme/utils/antdTokenNames.test.ts
**Line:** 86:86
**Comment:**
*Logic Error: The test for `getAllValidTokenNames` assumes that the
Superset token list contains "fontUrls", but since this token is not actually
part of the `SUPERSET_CUSTOM_TOKENS` set in the implementation, the assertion
will fail, making the test suite inconsistent with real behavior.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]