mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
fix(tests): prevent jest hangs caused by MessageChannel-mocked React scheduler (#39957)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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(<Select {...defaultProps} allowNewOptions />);
|
||||
|
||||
await open();
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('QueryAutoRefresh', () => {
|
||||
const refreshApi = 'glob:*/api/v1/query/updated_since?*';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('FilterScope TreeInitialization', () => {
|
||||
let formRef: { current: FormInstance | null };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
formRef = { current: null };
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('FilterScope TreeSelection', () => {
|
||||
let formRef: { current: FormInstance | null };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
formRef = { current: null };
|
||||
});
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -62,7 +62,7 @@ const props = {
|
||||
|
||||
beforeEach(() => {
|
||||
mockJsonFormsChangeTriggered = false;
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
mockedGet.mockReset();
|
||||
mockedPost.mockReset();
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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({}, '', '/');
|
||||
|
||||
|
||||
@@ -115,6 +115,12 @@ type LaunchQueue = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const pendingTimerIds = new Set<ReturnType<typeof setTimeout>>();
|
||||
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<void>)
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user