diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx
index 2f8cb6860a2..2b558b072cb 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx
@@ -1160,7 +1160,7 @@ test('does not fire onChange if the same value is selected in single mode', asyn
// Reference for the bug this tests: https://github.com/apache/superset/pull/33043#issuecomment-2809419640
test('typing and deleting the last character for a new option displays correctly', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
render();
await open();
diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx
index 0439157b055..ac388eccd64 100644
--- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx
@@ -53,7 +53,7 @@ describe('QueryAutoRefresh', () => {
const refreshApi = 'glob:*/api/v1/query/updated_since?*';
beforeEach(() => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
});
afterEach(() => {
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
index 3938cd8eafe..3f2addf4f70 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
@@ -45,7 +45,7 @@ fetchMock.get('glob:*/api/v1/dataset/?*', {
dataset_count: 3,
});
-jest.useFakeTimers();
+jest.useFakeTimers({ advanceTimers: true });
// Mock the user
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
index a7d7c2fb2b1..6829c8fe853 100644
--- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
@@ -611,7 +611,7 @@ test('should refresh the charts', async () => {
});
test('auto-refresh uses onRefresh with skipped filters and toggles refresh state', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
onRefresh.mockResolvedValue(undefined);
const originalRequestAnimationFrame = window.requestAnimationFrame;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
index d00093f0c51..3f2166fed14 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
@@ -39,7 +39,7 @@ import FilterBar from '.';
import { FILTERS_CONFIG_MODAL_TEST_ID } from '../FiltersConfigModal/FiltersConfigModal';
import * as dataMaskActions from 'src/dataMask/actions';
-jest.useFakeTimers();
+jest.useFakeTimers({ advanceTimers: true });
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeInitialization.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeInitialization.test.tsx
index 6e17a81b658..42e40455ff9 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeInitialization.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeInitialization.test.tsx
@@ -31,7 +31,7 @@ describe('FilterScope TreeInitialization', () => {
let formRef: { current: FormInstance | null };
beforeEach(() => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
formRef = { current: null };
});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeSelection.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeSelection.test.tsx
index b19c61b6d8e..c18fef2b5b1 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeSelection.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/__tests__/TreeSelection.test.tsx
@@ -31,7 +31,7 @@ describe('FilterScope TreeSelection', () => {
let formRef: { current: FormInstance | null };
beforeEach(() => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
formRef = { current: null };
});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/TimeGrainPreFilter.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/TimeGrainPreFilter.test.tsx
index 70890a62b91..0520d9e486d 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/TimeGrainPreFilter.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/TimeGrainPreFilter.test.tsx
@@ -50,7 +50,7 @@ const TIME_GRAIN_TUPLES: [string, string][] = [
// "state update on unmounted component" warnings. Scoped fake timers let us
// clear pending work deterministically during teardown for this test only.
beforeEach(() => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
});
afterEach(() => {
diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx
index 3ea0fb106f2..0f196928efa 100644
--- a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx
@@ -28,7 +28,7 @@ import {
import { CustomFrame } from '../components';
const TODAY = '2024-06-03';
-jest.useFakeTimers();
+jest.useFakeTimers({ advanceTimers: true });
jest.setSystemTime(new Date(TODAY).getTime());
const emptyValue = '';
diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx
index e35619772c2..a0cad6f63a6 100644
--- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx
+++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx
@@ -45,7 +45,7 @@ import VizTypeControl, { VIZ_TYPE_CONTROL_TEST_ID } from './index';
// Mock scrollIntoView to avoid errors in test environment
jest.mock('scroll-into-view-if-needed', () => jest.fn());
-jest.useFakeTimers();
+jest.useFakeTimers({ advanceTimers: true });
class MainPreset extends Preset {
constructor() {
diff --git a/superset-frontend/src/features/semanticLayers/SemanticLayerModal.test.tsx b/superset-frontend/src/features/semanticLayers/SemanticLayerModal.test.tsx
index f664c30fffc..a3aed93fde7 100644
--- a/superset-frontend/src/features/semanticLayers/SemanticLayerModal.test.tsx
+++ b/superset-frontend/src/features/semanticLayers/SemanticLayerModal.test.tsx
@@ -62,7 +62,7 @@ const props = {
beforeEach(() => {
mockJsonFormsChangeTriggered = false;
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
mockedGet.mockReset();
mockedPost.mockReset();
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
index e0f7824b2be..8561ff8b7a5 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
@@ -37,7 +37,7 @@ import {
PluginFilterSelectQueryFormData,
} from './types';
-jest.useFakeTimers();
+jest.useFakeTimers({ advanceTimers: true });
const selectMultipleProps = {
formData: {
@@ -1286,7 +1286,7 @@ test('resets dependent filter to first item when value does not exist in data',
});
test('renders text input instead of dropdown when operatorType is ILIKE contains', () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const props = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
@@ -1316,7 +1316,7 @@ test('renders text input instead of dropdown when operatorType is ILIKE contains
});
test('renders text input with starts-with placeholder', () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const props = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.StartsWith },
@@ -1345,7 +1345,7 @@ test('renders text input with starts-with placeholder', () => {
});
test('typing in LIKE input calls setDataMask with ILIKE Contains payload', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const props = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
@@ -1391,7 +1391,7 @@ test('typing in LIKE input calls setDataMask with ILIKE Contains payload', async
});
test('typing in LIKE input with inverse selection calls setDataMask with NOT ILIKE payload', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const props = buildSelectFilterProps({
formData: {
@@ -1440,7 +1440,7 @@ test('typing in LIKE input with inverse selection calls setDataMask with NOT ILI
});
test('clear-all resets LIKE input value and calls setDataMask with empty state', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const likeProps = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
@@ -1506,7 +1506,7 @@ test('clear-all resets LIKE input value and calls setDataMask with empty state',
});
test('pending LIKE debounce still applies after rerender recreates updateDataMask', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const likeProps = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
@@ -1573,7 +1573,7 @@ test('pending LIKE debounce still applies after rerender recreates updateDataMas
});
test('pending LIKE debounce is canceled when operatorType switches back to Exact', async () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const likeProps = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Contains },
@@ -1628,7 +1628,7 @@ test('pending LIKE debounce is canceled when operatorType switches back to Exact
});
test('renders standard Select dropdown when operatorType is Exact', () => {
- jest.useFakeTimers();
+ jest.useFakeTimers({ advanceTimers: true });
const setDataMaskMock = jest.fn();
const props = buildSelectFilterProps({
formData: { operatorType: SelectFilterOperatorType.Exact },
diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
index c17f2c994eb..2d1c4752e9b 100644
--- a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
@@ -42,15 +42,16 @@ beforeEach(() => {
});
afterEach(async () => {
+ // Restore real timers FIRST so the flush below uses real setTimeout,
+ // preventing a deadlock if a test threw while fake timers were active.
+ jest.useRealTimers();
+
// Flush pending React state updates within act() to prevent warnings
// and "document global undefined" errors from async operations
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
- // Restore real timers in case a test using fake timers threw early
- jest.useRealTimers();
-
// Reset browser history state to prevent query params leaking between tests
window.history.replaceState({}, '', '/');
diff --git a/superset-frontend/src/pages/FileHandler/index.test.tsx b/superset-frontend/src/pages/FileHandler/index.test.tsx
index bc4d9fad80a..a337a88a708 100644
--- a/superset-frontend/src/pages/FileHandler/index.test.tsx
+++ b/superset-frontend/src/pages/FileHandler/index.test.tsx
@@ -115,6 +115,12 @@ type LaunchQueue = {
) => void;
};
+const pendingTimerIds = new Set>();
+const MAX_CONSUMER_POLL_ATTEMPTS = 50;
+
+// Defer the consumer call to a macrotask so it doesn't fire synchronously inside
+// the component's useEffect — calling it inline deadlocks Jest because the
+// MessageChannel mock in jsDomWithFetchAPI forces React to schedule via setTimeout.
const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
let savedConsumer:
| ((params: { files?: MockFileHandle[] }) => void | Promise)
@@ -123,7 +129,11 @@ const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
setConsumer: (consumer: (params: { files?: MockFileHandle[] }) => void) => {
savedConsumer = consumer;
if (fileHandle) {
- consumer({ files: [fileHandle] });
+ const id = setTimeout(() => {
+ pendingTimerIds.delete(id);
+ consumer({ files: [fileHandle] });
+ }, 0);
+ pendingTimerIds.add(id);
}
},
};
@@ -132,25 +142,34 @@ const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
// In slower CI runners, useEffect may not have registered the consumer yet.
// Wait briefly for it before triggering.
let attempts = 0;
- while (!savedConsumer && attempts < 50) {
+ while (!savedConsumer && attempts < MAX_CONSUMER_POLL_ATTEMPTS) {
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => {
setTimeout(resolve, 0);
});
attempts += 1;
}
- await savedConsumer?.(params);
+ if (!savedConsumer) {
+ throw new Error(
+ `LaunchQueue consumer was never registered after ${MAX_CONSUMER_POLL_ATTEMPTS} polling attempts`,
+ );
+ }
+ await savedConsumer(params);
},
};
};
beforeEach(() => {
jest.clearAllMocks();
- delete (window as any).launchQueue;
+ delete (window as unknown as Window & { launchQueue?: LaunchQueue })
+ .launchQueue;
});
afterEach(() => {
- delete (window as any).launchQueue;
+ pendingTimerIds.forEach(id => clearTimeout(id));
+ pendingTimerIds.clear();
+ delete (window as unknown as Window & { launchQueue?: LaunchQueue })
+ .launchQueue;
});
test('shows error when launchQueue is not supported', async () => {