diff --git a/package.json b/package.json index c837fd2a54a357..0788a8e265b1ae 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "karma-firefox-launcher": "^2.1.3", "karma-mocha": "^2.0.1", "karma-sourcemap-loader": "^0.4.0", + "karma-spec-reporter": "^0.0.36", "karma-webpack": "^5.0.0", "lerna": "^8.2.2", "lodash": "^4.17.21", diff --git a/packages-internal/test-utils/src/createRenderer.tsx b/packages-internal/test-utils/src/createRenderer.tsx index f6f9cfa64cf367..98b75680153075 100644 --- a/packages-internal/test-utils/src/createRenderer.tsx +++ b/packages-internal/test-utils/src/createRenderer.tsx @@ -18,7 +18,6 @@ import { import { userEvent } from '@testing-library/user-event'; import * as React from 'react'; import * as ReactDOMServer from 'react-dom/server'; -import { useFakeTimers } from 'sinon'; import reactMajor from './reactMajor'; interface Interaction { @@ -346,149 +345,25 @@ const isVitest = // VITEST_BROWSER_DEBUG is present on vitest in browser mode. typeof process.env.VITEST_BROWSER_DEBUG !== 'undefined'; -function createVitestClock( - defaultMode: 'fake' | 'real', - config: ClockConfig, - options: Exclude[0], number | Date>, - vi: any, -): Clock { - if (defaultMode === 'fake') { - beforeEach(() => { - vi.useFakeTimers(options); - if (config) { - vi.setSystemTime(config); - } - }); - afterEach(() => { - vi.useRealTimers(); - }); - } else { - beforeEach(() => { - if (config) { - vi.setSystemTime(config); - } - }); - afterEach(() => { - vi.useRealTimers(); - }); - } - +function createClock(): Clock { return { - withFakeTimers: () => { - beforeEach(() => { - vi.useFakeTimers(options); - }); - afterEach(() => { - vi.useRealTimers(); - }); - }, - runToLast: () => { - traceSync('runToLast', () => { - rtlAct(() => { - vi.runOnlyPendingTimers(); - }); - }); - }, - isReal() { - return !vi.isFakeTimers(); - }, - restore() { - vi.useRealTimers(); - }, - tick(timeoutMS: number) { - traceSync('tick', () => { - rtlAct(() => { - vi.advanceTimersByTime(timeoutMS); - }); - }); + tick() { + throw new Error('Unsupported'); }, runAll() { - traceSync('runAll', () => { - rtlAct(() => { - vi.runAllTimers(); - }); - }); - }, - }; -} - -function createClock( - defaultMode: 'fake' | 'real', - config: ClockConfig, - options: Exclude[0], number | Date>, - vi: any, -): Clock { - if (isVitest) { - return createVitestClock(defaultMode, config, options, vi); - } - - let clock: ReturnType | null = null; - - let mode = defaultMode; - - beforeEach(() => { - if (mode === 'fake') { - clock = useFakeTimers({ - now: config, - // useIsFocusVisible schedules a global timer that needs to persist regardless of whether components are mounted or not. - // Technically we'd want to reset all modules between tests but we don't have that technology. - // In the meantime just continue to clear native timers like with did for the past years when using `sinon` < 8. - shouldClearNativeTimers: true, - ...options, - }); - } - }); - - afterEach(() => { - clock?.restore(); - clock = null; - }); - - return { - tick(timeoutMS: number) { - if (clock === null) { - throw new Error(`Can't advance the real clock. Did you mean to call this on fake clock?`); - } - traceSync('tick', () => { - rtlAct(() => { - clock!.tick(timeoutMS); - }); - }); - }, - runAll() { - if (clock === null) { - throw new Error(`Can't advance the real clock. Did you mean to call this on fake clock?`); - } - traceSync('runAll', () => { - rtlAct(() => { - clock!.runAll(); - }); - }); + throw new Error('Unsupported'); }, runToLast() { - if (clock === null) { - throw new Error(`Can't advance the real clock. Did you mean to call this on fake clock?`); - } - traceSync('runToLast', () => { - rtlAct(() => { - clock!.runToLast(); - }); - }); + throw new Error('Unsupported'); }, isReal() { - return setTimeout.hasOwnProperty('clock') === false; + throw new Error('Unsupported'); }, withFakeTimers() { - before(() => { - mode = 'fake'; - }); - - after(() => { - mode = defaultMode; - }); + throw new Error('Unsupported'); }, restore() { - clock?.restore(); + throw new Error('Unsupported'); }, }; } @@ -503,12 +378,6 @@ export interface Renderer { } export interface CreateRendererOptions extends Pick { - /** - * @default 'real' - */ - clock?: 'fake' | 'real'; - clockConfig?: ClockConfig; - clockOptions?: Parameters[2]; /** * Vitest needs to be injected because this file is transpiled to commonjs and vitest is an esm module. * @default {} @@ -517,17 +386,11 @@ export interface CreateRendererOptions extends Pick { - if (!clock.isReal()) { - const error = new Error( - "Can't cleanup before fake timers are restored.\n" + - 'Be sure to:\n' + - ' 1. Only use `clock` from `createRenderer`.\n' + - ' 2. Call `createRenderer` in a suite and not any test hook (for example `beforeEach`) or test itself (for example `it`).', - ); - // Use saved stack otherwise the stack trace will not include the test location. - error.stack = createClientRenderStack; - throw error; - } - cleanup(); profiler.report(); profiler = null!; diff --git a/packages-internal/test-utils/src/focusVisible.ts b/packages-internal/test-utils/src/focusVisible.ts index 1c6e10b23d1c29..9ef8f4934b5737 100644 --- a/packages-internal/test-utils/src/focusVisible.ts +++ b/packages-internal/test-utils/src/focusVisible.ts @@ -1,7 +1,7 @@ import { act, fireEvent } from './createRenderer'; -export default function focusVisible(element: HTMLElement) { - act(() => { +export default async function focusVisible(element: HTMLElement) { + await act(async () => { element.blur(); fireEvent.keyDown(document.body, { key: 'Tab' }); element.focus(); diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx index b8ff2c801884f9..adb6f340d46e03 100644 --- a/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx +++ b/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx @@ -97,18 +97,18 @@ describe('Joy ', () => { }); describe('combobox', () => { - it('should clear the input when blur', () => { + it('should clear the input when blur', async () => { const { getByRole } = render(); const input = getByRole('combobox') as HTMLInputElement; - act(() => { + await act(async () => { input.focus(); fireEvent.change(document.activeElement!, { target: { value: 'a' } }); }); expect(input.value).to.equal('a'); - act(() => { + await act(async () => { (document.activeElement as HTMLElement).blur(); }); expect(input.value).to.equal(''); @@ -237,7 +237,7 @@ describe('Joy ', () => { }); describe('prop: limitTags', () => { - it('show all items on focus', () => { + it('show all items on focus', async () => { const { container, getAllByRole, getByRole } = render( ', () => { // include hidden clear button because JSDOM thinks it's visible expect(getAllByRole('button', { hidden: true })).to.have.lengthOf(4); - act(() => { + await act(async () => { getByRole('combobox').focus(); }); expect(container.textContent).to.equal('onetwothree'); @@ -261,7 +261,7 @@ describe('Joy ', () => { } }); - it('show 0 item on close when set 0 to limitTags', () => { + it('show 0 item on close when set 0 to limitTags', async () => { const { container, getAllByRole, getByRole } = render( ', () => { // include hidden clear button because JSDOM thinks it's visible expect(getAllByRole('button', { hidden: true })).to.have.lengthOf(2); - act(() => { + await act(async () => { getByRole('combobox').focus(); }); expect(container.textContent).to.equal('onetwothree'); @@ -287,7 +287,7 @@ describe('Joy ', () => { }); describe('prop: filterSelectedOptions', () => { - it('when the last item is selected, highlights the new last item', () => { + it('when the last item is selected, highlights the new last item', async () => { const { getByRole } = render( ', () => { fireEvent.keyDown(textbox, { key: 'ArrowUp' }); checkHighlightIs(getByRole('listbox'), 'three'); fireEvent.keyDown(textbox, { key: 'Enter' }); // selects the last option (three) - act(() => { + await act(async () => { textbox.blur(); textbox.focus(); // opens the listbox again }); @@ -310,7 +310,7 @@ describe('Joy ', () => { }); describe('prop: autoSelect', () => { - it('should not clear on blur when value does not match any option', () => { + it('should not clear on blur when value does not match any option', async () => { const handleChange = spy(); const options = ['one', 'two']; render( @@ -321,7 +321,7 @@ describe('Joy ', () => { fireEvent.change(textbox, { target: { value: 'o' } }); fireEvent.keyDown(textbox, { key: 'ArrowDown' }); fireEvent.change(textbox, { target: { value: 'oo' } }); - act(() => { + await act(async () => { textbox.blur(); }); @@ -329,7 +329,7 @@ describe('Joy ', () => { expect(handleChange.args[0][1]).to.deep.equal('oo'); }); - it('should add new value when autoSelect & multiple on blur', () => { + it('should add new value when autoSelect & multiple on blur', async () => { const handleChange = spy(); const options = ['one', 'two']; render( @@ -345,7 +345,7 @@ describe('Joy ', () => { ); const textbox = screen.getByRole('combobox'); - act(() => { + await act(async () => { fireEvent.change(textbox, { target: { value: 't' } }); fireEvent.keyDown(textbox, { key: 'ArrowDown' }); textbox.blur(); @@ -355,7 +355,7 @@ describe('Joy ', () => { expect(handleChange.args[0][1]).to.deep.equal(options); }); - it('should add new value when autoSelect & multiple & freeSolo on blur', () => { + it('should add new value when autoSelect & multiple & freeSolo on blur', async () => { const handleChange = spy(); render( ', () => { ); fireEvent.change(document.activeElement!, { target: { value: 'a' } }); - act(() => { + await act(async () => { (document.activeElement as HTMLElement).blur(); }); @@ -379,11 +379,11 @@ describe('Joy ', () => { }); describe('prop: multiple', () => { - it('should not crash', () => { + it('should not crash', async () => { const { getByRole } = render(); const input = getByRole('combobox'); - act(() => { + await act(async () => { input.focus(); (document.activeElement as HTMLElement).blur(); input.focus(); @@ -831,29 +831,29 @@ describe('Joy ', () => { }); describe('prop: clearOnBlur', () => { - it('should clear on blur', () => { + it('should clear on blur', async () => { render(); const textbox = screen.getByRole('combobox') as HTMLInputElement; fireEvent.change(textbox, { target: { value: 'test' } }); expect((document.activeElement as HTMLInputElement).value).to.equal('test'); - act(() => { + await act(async () => { textbox.blur(); }); expect(textbox.value).to.equal(''); }); - it('should not clear on blur', () => { + it('should not clear on blur', async () => { render(); const textbox = screen.getByRole('combobox') as HTMLInputElement; fireEvent.change(textbox, { target: { value: 'test' } }); expect((document.activeElement as HTMLInputElement).value).to.equal('test'); - act(() => { + await act(async () => { textbox.blur(); }); expect(textbox.value).to.equal('test'); }); - it('should not clear on blur with `multiple` enabled', () => { + it('should not clear on blur with `multiple` enabled', async () => { render( ', () => { const textbox = screen.getByRole('combobox') as HTMLInputElement; fireEvent.change(textbox, { target: { value: 'test' } }); expect((document.activeElement as HTMLInputElement).value).to.equal('test'); - act(() => { + await act(async () => { textbox.blur(); }); expect(textbox.value).to.equal('test'); @@ -947,7 +947,7 @@ describe('Joy ', () => { }); describe('prop: openOnFocus', () => { - it('enables open on input focus', () => { + it('enables open on input focus', async () => { const { getByRole } = render( , ); @@ -960,7 +960,7 @@ describe('Joy ', () => { fireEvent.click(textbox); expect(textbox).to.have.attribute('aria-expanded', 'false'); - act(() => { + await act(async () => { (document.activeElement as HTMLElement).blur(); }); @@ -1104,10 +1104,10 @@ describe('Joy ', () => { expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); }); - it('should close the popup when disabled is true', () => { + it('should close the popup when disabled is true', async () => { const { setProps } = render(); const textbox = screen.getByRole('combobox'); - act(() => { + await act(async () => { textbox.focus(); }); fireEvent.keyDown(textbox, { key: 'ArrowDown' }); @@ -1116,12 +1116,12 @@ describe('Joy ', () => { expect(screen.queryByRole('listbox')).to.equal(null); }); - it('should not crash when autoSelect & freeSolo are set, text is focused & disabled gets truthy', () => { + it('should not crash when autoSelect & freeSolo are set, text is focused & disabled gets truthy', async () => { const { setProps } = render( , ); const textbox = screen.getByRole('combobox'); - act(() => { + await act(async () => { textbox.focus(); }); setProps({ disabled: true }); @@ -1180,6 +1180,7 @@ describe('Joy ', () => { { id: '10', text: 'One' }, { id: '20', text: 'Two' }, ]; + const options = [ { id: '10', text: 'One' }, { id: '20', text: 'Two' }, @@ -1235,6 +1236,7 @@ describe('Joy ', () => { { group: 2, value: 'F' }, { group: 1, value: 'C' }, ]; + expect(() => { render( ', () => { }); describe('prop: options', () => { - it('should scroll selected option into view when multiple elements with role as listbox available', function test() { + it('should scroll selected option into view when multiple elements with role as listbox available', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { this.skip(); } @@ -1304,6 +1306,7 @@ describe('Joy ', () => { }} autoFocus /> + ', () => { expect(container.firstChild).to.have.class(classes.disabled); }); - it('should reset the focused state if getting disabled', () => { + it('should reset the focused state if getting disabled', async () => { const handleBlur = spy(); const handleFocus = spy(); const { getByRole, setProps } = render(); - act(() => { + await act(async () => { getByRole('textbox').focus(); }); expect(handleFocus.callCount).to.equal(1); @@ -107,14 +107,14 @@ describe('Joy ', () => { }); describe('slotProps: input', () => { - it('`onKeyDown` and `onKeyUp` should work', () => { + it('`onKeyDown` and `onKeyUp` should work', async () => { const handleKeyDown = spy(); const handleKeyUp = spy(); const { getByRole } = render( , ); - act(() => { + await act(async () => { getByRole('textbox').focus(); }); fireEvent.keyDown(getByRole('textbox')); @@ -124,31 +124,31 @@ describe('Joy ', () => { expect(handleKeyUp.callCount).to.equal(1); }); - it('should call focus and blur', () => { + it('should call focus and blur', async () => { const handleBlur = spy(); const handleFocus = spy(); const { getByRole } = render( , ); - act(() => { + await act(async () => { getByRole('textbox').focus(); }); expect(handleFocus.callCount).to.equal(1); - act(() => { + await act(async () => { getByRole('textbox').blur(); }); expect(handleFocus.callCount).to.equal(1); }); - it('should override outer handlers', () => { + it('should override outer handlers', async () => { const handleFocus = spy(); const handleSlotFocus = spy(); const { getByRole } = render( , ); - act(() => { + await act(async () => { getByRole('textbox').focus(); }); expect(handleFocus.callCount).to.equal(0); diff --git a/packages/mui-joy/src/Link/Link.test.tsx b/packages/mui-joy/src/Link/Link.test.tsx index 0da69c7a560910..58ce4e3651f2bc 100644 --- a/packages/mui-joy/src/Link/Link.test.tsx +++ b/packages/mui-joy/src/Link/Link.test.tsx @@ -8,8 +8,8 @@ import Typography from '@mui/joy/Typography'; import { ThemeProvider, TypographySystem } from '@mui/joy/styles'; import describeConformance from '../../test/describeConformance'; -function focusVisible(element: HTMLAnchorElement | null) { - act(() => { +async function focusVisible(element: HTMLAnchorElement | null) { + await act(async () => { element?.blur(); document.dispatchEvent(new window.Event('keydown')); element?.focus(); @@ -75,7 +75,7 @@ describe('', () => { }); describe('keyboard focus', () => { - it('should add the focusVisible class when focused', function test() { + it('should add the focusVisible class when focused', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // JSDOM doesn't support :focus-visible this.skip(); @@ -86,11 +86,11 @@ describe('', () => { expect(anchor).not.to.have.class(classes.focusVisible); - focusVisible(anchor); + await focusVisible(anchor); expect(anchor).to.have.class(classes.focusVisible); - act(() => { + await act(async () => { anchor?.blur(); }); diff --git a/packages/mui-joy/src/ListItemButton/ListItemButton.test.tsx b/packages/mui-joy/src/ListItemButton/ListItemButton.test.tsx index 08f3eb26a2c20d..213dd9f4d1254b 100644 --- a/packages/mui-joy/src/ListItemButton/ListItemButton.test.tsx +++ b/packages/mui-joy/src/ListItemButton/ListItemButton.test.tsx @@ -56,7 +56,7 @@ describe('Joy ', () => { }); describe('prop: focusVisibleClassName', () => { - it('should have focusVisible classes', function test() { + it('should have focusVisible classes', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // JSDOM doesn't support :focus-visible this.skip(); @@ -65,7 +65,7 @@ describe('Joy ', () => { const { getByRole } = render(); const button = getByRole('button'); - act(() => { + await act(async () => { fireEvent.keyDown(document.activeElement || document.body, { key: 'Tab' }); button.focus(); }); diff --git a/packages/mui-joy/src/Menu/Menu.test.tsx b/packages/mui-joy/src/Menu/Menu.test.tsx index 8a8e9f38ab4222..261036e2e5e4c0 100644 --- a/packages/mui-joy/src/Menu/Menu.test.tsx +++ b/packages/mui-joy/src/Menu/Menu.test.tsx @@ -21,7 +21,7 @@ const testContext: DropdownContextValue = { }; describe('Joy ', () => { - const { render } = createRenderer({ clock: 'fake' }); + const { render } = createRenderer(); describeConformance(, () => ({ classes, @@ -54,7 +54,7 @@ describe('Joy ', () => { expect(screen.getByTestId('popover')).to.have.tagName('ul'); }); - it('should pass onClose prop to Popover', () => { + it('should pass onClose prop to Popover', async () => { const handleClose = spy(); render( @@ -67,7 +67,7 @@ describe('Joy ', () => { const item = screen.getByRole('menuitem'); - act(() => { + await act(async () => { item.focus(); }); diff --git a/packages/mui-joy/src/Modal/Modal.test.tsx b/packages/mui-joy/src/Modal/Modal.test.tsx index 422cdde727952c..31eadbf4f4332e 100644 --- a/packages/mui-joy/src/Modal/Modal.test.tsx +++ b/packages/mui-joy/src/Modal/Modal.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { spy } from 'sinon'; +import { SinonFakeTimers, spy, useFakeTimers } from 'sinon'; import { expect } from 'chai'; import { createRenderer, act, fireEvent, within } from '@mui/internal-test-utils'; import { ThemeProvider } from '@mui/joy/styles'; @@ -8,7 +8,7 @@ import Modal, { modalClasses as classes, ModalProps } from '@mui/joy/Modal'; import describeConformance from '../../test/describeConformance'; describe('', () => { - const { clock, render } = createRenderer(); + const { render } = createRenderer(); describeConformance( @@ -178,14 +178,14 @@ describe('', () => { }); describe('event: keydown', () => { - it('when mounted, TopModal and event not esc should not call given functions', () => { + it('when mounted, TopModal and event not esc should not call given functions', async () => { const onCloseSpy = spy(); const { getByTestId } = render(
, ); - act(() => { + await act(async () => { getByTestId('modal').focus(); }); @@ -196,7 +196,7 @@ describe('', () => { expect(onCloseSpy).to.have.property('callCount', 0); }); - it('should call onClose when Esc is pressed and stop event propagation', () => { + it('should call onClose when Esc is pressed and stop event propagation', async () => { const handleKeyDown = spy(); const onCloseSpy = spy(); const { getByTestId } = render( @@ -206,7 +206,7 @@ describe('', () => {
, ); - act(() => { + await act(async () => { getByTestId('modal').focus(); }); @@ -218,7 +218,7 @@ describe('', () => { expect(handleKeyDown).to.have.property('callCount', 0); }); - it('should not call onClose when `disableEscapeKeyDown={true}`', () => { + it('should not call onClose when `disableEscapeKeyDown={true}`', async () => { const handleKeyDown = spy(); const onCloseSpy = spy(); const { getByTestId } = render( @@ -228,7 +228,7 @@ describe('', () => { , ); - act(() => { + await act(async () => { getByTestId('modal').focus(); }); @@ -285,11 +285,11 @@ describe('', () => { describe('focus', () => { let initialFocus: null | HTMLButtonElement = null; - beforeEach(() => { + beforeEach(async () => { initialFocus = document.createElement('button'); initialFocus.tabIndex = 0; document.body.appendChild(initialFocus); - act(() => { + await act(async () => { initialFocus?.focus(); }); }); @@ -320,7 +320,7 @@ describe('', () => { , // TODO: https://github.com/reactwg/react-18/discussions/18#discussioncomment-893076 - { strictEffects: false }, + { strict: false, strictEffects: false }, ); expect(getByTestId('auto-focus')).toHaveFocus(); @@ -355,9 +355,29 @@ describe('', () => { }); describe('focus stealing', () => { - clock.withFakeTimers(); + let timer: SinonFakeTimers | null = null; + + beforeEach(() => { + timer = useFakeTimers({ + shouldClearNativeTimers: true, + toFake: [ + 'performance', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'Date', + 'requestAnimationFrame', + 'cancelAnimationFrame', + ], + }); + }); + + afterEach(() => { + timer?.restore(); + }); - it('does not steal focus from other frames', function test() { + it('does not steal focus from other frames', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // TODO: Unclear why this fails. Not important // since a browser test gives us more confidence anyway @@ -412,18 +432,40 @@ describe('', () => { , ); - act(() => { + await act(async () => { getByTestId('foreign-input').focus(); }); // wait for the `contain` interval check to kick in. - clock.tick(500); + await act(async () => { + timer?.tickAsync(500); + }); expect(getByTestId('foreign-input')).toHaveFocus(); }); }); describe('when starting open and closing immediately', () => { - clock.withFakeTimers(); + let timer: SinonFakeTimers | null = null; + + beforeEach(() => { + timer = useFakeTimers({ + shouldClearNativeTimers: true, + toFake: [ + 'performance', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'Date', + 'requestAnimationFrame', + 'cancelAnimationFrame', + ], + }); + }); + + afterEach(() => { + timer?.restore(); + }); // Test case for https://github.com/mui/material-ui/issues/12831 it('should unmount the children ', () => { @@ -447,7 +489,27 @@ describe('', () => { }); describe('two modal at the same time', () => { - clock.withFakeTimers(); + let timer: SinonFakeTimers | null = null; + + beforeEach(() => { + timer = useFakeTimers({ + shouldClearNativeTimers: true, + toFake: [ + 'performance', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'Date', + 'requestAnimationFrame', + 'cancelAnimationFrame', + ], + }); + }); + + afterEach(() => { + timer?.restore(); + }); it('should open and close', () => { function TestCase(props: { open: boolean }) { diff --git a/packages/mui-joy/src/Radio/Radio.test.tsx b/packages/mui-joy/src/Radio/Radio.test.tsx index fdb63ce9459b5c..7192555bbe6cfb 100644 --- a/packages/mui-joy/src/Radio/Radio.test.tsx +++ b/packages/mui-joy/src/Radio/Radio.test.tsx @@ -98,11 +98,11 @@ describe('', () => { expect(getByRole('radio')).to.have.property('disabled', true); }); - it('the Checked state changes after change events', () => { + it('the Checked state changes after change events', async () => { const { getByRole } = render(); // how a user would trigger it - act(() => { + await act(async () => { getByRole('radio').click(); fireEvent.change(getByRole('radio'), { target: { checked: '' } }); }); @@ -148,7 +148,7 @@ describe('', () => { expect(getByRole('radio', { checked: true })).to.have.property('value', '1'); }); - it('should be checked when changing the value', () => { + it('should be checked when changing the value', async () => { const { getByRole } = render( @@ -158,13 +158,13 @@ describe('', () => { expect(getByRole('radio', { checked: true })).to.have.property('value', '1'); - act(() => { + await act(async () => { getByRole('radio', { checked: false }).click(); }); expect(getByRole('radio', { checked: true })).to.have.property('value', '0'); - act(() => { + await act(async () => { getByRole('radio', { checked: false }).click(); }); diff --git a/packages/mui-joy/src/RadioGroup/RadioGroup.test.tsx b/packages/mui-joy/src/RadioGroup/RadioGroup.test.tsx index 8665f6bd97ad82..90b851a22ebfb4 100644 --- a/packages/mui-joy/src/RadioGroup/RadioGroup.test.tsx +++ b/packages/mui-joy/src/RadioGroup/RadioGroup.test.tsx @@ -48,12 +48,12 @@ describe('', () => { expect(handleBlur.callCount).to.equal(1); }); - it('should fire the onKeyDown callback', () => { + it('should fire the onKeyDown callback', async () => { const handleKeyDown = spy(); const { getByRole } = render(); const radiogroup = getByRole('radiogroup'); - act(() => { + await act(async () => { radiogroup.focus(); }); diff --git a/packages/mui-joy/src/Select/Select.test.tsx b/packages/mui-joy/src/Select/Select.test.tsx index bfe89587c6bdbf..3f7a1c6e25a60a 100644 --- a/packages/mui-joy/src/Select/Select.test.tsx +++ b/packages/mui-joy/src/Select/Select.test.tsx @@ -11,7 +11,7 @@ import ListDivider from '@mui/joy/ListDivider'; import describeConformance from '../../test/describeConformance'; describe('Joy , () => ({ render, @@ -86,7 +86,7 @@ describe('Joy ', () => { , ); const select = getByRole('combobox'); - act(() => { + await act(async () => { select.focus(); }); - act(() => { + await act(async () => { select.blur(); }); @@ -114,7 +114,7 @@ describe('Joy ', () => { , ); - act(() => { + await act(async () => { screen.getByRole('option', { selected: true }).click(); }); @@ -141,7 +141,7 @@ describe('Joy @@ -151,7 +151,7 @@ describe('Joy , ); fireEvent.click(getByRole('combobox')); - act(() => { + await act(async () => { getAllByRole('option')[1].click(); }); @@ -159,7 +159,7 @@ describe('Joy @@ -169,7 +169,7 @@ describe('Joy , ); fireEvent.click(getByRole('combobox')); - act(() => { + await act(async () => { getAllByRole('option')[1].click(); }); @@ -443,7 +443,7 @@ describe('Joy ', () => { ); const options = screen.getAllByRole('option'); - act(() => { + await act(async () => { options[0].click(); }); @@ -465,7 +465,7 @@ describe('Joy @@ -476,7 +476,7 @@ describe('Joy ', () => { }); describe('form submission', () => { - it('includes the Select value in the submitted form data when the `name` attribute is provided', function test() { + it('includes the Select value in the submitted form data when the `name` attribute is provided', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // FormData is not available in JSDOM this.skip(); @@ -522,14 +522,14 @@ describe('Joy ', () => { ); const button = getByText('Submit'); - act(() => { + await act(async () => { button.click(); }); expect(isEventHandled).to.equal(true); }); - it('formats the object values as JSON before posting', function test() { + it('formats the object values as JSON before posting', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // FormData is not available in JSDOM this.skip(); @@ -599,7 +599,7 @@ describe('Joy ', () => { }); }); - it('should show dropdown if the children of the select button is clicked', () => { + it('should show dropdown if the children of the select button is clicked', async () => { const { getByTestId, getByRole } = render( , ); // Fire Click of the avatar - act(() => { + await act(async () => { fireEvent.click(getByTestId('test-element')); }); expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'true'); // click again should close - act(() => { + await act(async () => { fireEvent.click(getByTestId('test-element')); }); expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'false'); diff --git a/packages/mui-joy/src/Snackbar/Snackbar.test.tsx b/packages/mui-joy/src/Snackbar/Snackbar.test.tsx index af55c7aa4e5d98..112e42a0473bde 100644 --- a/packages/mui-joy/src/Snackbar/Snackbar.test.tsx +++ b/packages/mui-joy/src/Snackbar/Snackbar.test.tsx @@ -1,13 +1,35 @@ import * as React from 'react'; import { expect } from 'chai'; -import { spy } from 'sinon'; +import { SinonFakeTimers, spy, useFakeTimers } from 'sinon'; import { createRenderer, fireEvent, act } from '@mui/internal-test-utils'; import Snackbar, { snackbarClasses as classes } from '@mui/joy/Snackbar'; import { ThemeProvider } from '@mui/joy/styles'; import describeConformance from '../../test/describeConformance'; describe('Joy ', () => { - const { render: clientRender, clock } = createRenderer({ clock: 'fake' }); + let timer: SinonFakeTimers | null = null; + + beforeEach(() => { + timer = useFakeTimers({ + shouldClearNativeTimers: true, + toFake: [ + 'performance', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'Date', + 'requestAnimationFrame', + 'cancelAnimationFrame', + ], + }); + }); + + afterEach(() => { + timer?.restore(); + }); + + const { render: clientRender } = createRenderer(); /** * @type {typeof plainRender extends (...args: infer T) => any ? T : never} args @@ -19,9 +41,11 @@ describe('Joy ', () => { * We have to defer the effect manually like `useEffect` would so we have to flush the effect manually instead of relying on `act()`. * React bug: https://github.com/facebook/react/issues/20074 */ - function render(...args: [React.ReactElement]) { + async function render(...args: [React.ReactElement]) { const result = clientRender(...args); - clock.tick(0); + await act(async () => { + await timer?.tickAsync(0); + }); return result; } @@ -30,7 +54,7 @@ describe('Joy ', () => { Hello World! , () => ({ - render, + render: clientRender, classes, ThemeProvider, muiName: 'JoySnackbar', @@ -46,24 +70,26 @@ describe('Joy ', () => { ); describe('prop: onClose', () => { - it('should be called when clicking away', () => { + it('should be called when clicking away', async () => { const handleClose = spy(); - render( + await render( Message , ); const event = new window.Event('click', { bubbles: true, cancelable: true }); - document.body.dispatchEvent(event); + await act(async () => { + document.body.dispatchEvent(event); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([event, 'clickaway']); }); - it('should be called when pressing Escape', () => { + it('should be called when pressing Escape', async () => { const handleClose = spy(); - render( + await render( Message , @@ -74,10 +100,10 @@ describe('Joy ', () => { expect(handleClose.args[0][1]).to.equal('escapeKeyDown'); }); - it('can limit which Snackbars are closed when pressing Escape', () => { + it('can limit which Snackbars are closed when pressing Escape', async () => { const handleCloseA = spy((event) => event.preventDefault()); const handleCloseB = spy(); - render( + await render( Message A @@ -96,10 +122,10 @@ describe('Joy ', () => { }); describe('prop: autoHideDuration', () => { - it('should call onClose when the timer is done', () => { + it('should call onClose when the timer is done', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - const { setProps } = render( + const { setProps } = await render( Message , @@ -109,35 +135,41 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration); + await act(async () => { + await timer?.tickAsync(autoHideDuration); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); }); - it('calls onClose at timeout even if the prop changes', () => { + it('calls onClose at timeout even if the prop changes', async () => { const handleClose1 = spy(); const handleClose2 = spy(); const autoHideDuration = 2e3; - const { setProps } = render( + const { setProps } = await render( Message , ); setProps({ open: true }); - clock.tick(autoHideDuration / 2); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); setProps({ open: true, onClose: handleClose2 }); - clock.tick(autoHideDuration / 2); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); expect(handleClose1.callCount).to.equal(0); expect(handleClose2.callCount).to.equal(1); }); - it('should not call onClose when the autoHideDuration is reset', () => { + it('should not call onClose when the autoHideDuration is reset', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - const { setProps } = render( + const { setProps } = await render( Message , @@ -147,17 +179,21 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration / 2); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); setProps({ autoHideDuration: undefined }); - clock.tick(autoHideDuration / 2); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); expect(handleClose.callCount).to.equal(0); }); - it('should not call onClose if autoHideDuration is undefined', () => { + it('should not call onClose if autoHideDuration is undefined', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - render( + await render( Message , @@ -165,16 +201,18 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration); + await act(async () => { + await timer?.tickAsync(autoHideDuration); + }); expect(handleClose.callCount).to.equal(0); }); - it('should not call onClose if autoHideDuration is null', () => { + it('should not call onClose if autoHideDuration is null', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - render( + await render( Message , @@ -182,16 +220,18 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration); + await act(async () => { + await timer?.tickAsync(autoHideDuration); + }); expect(handleClose.callCount).to.equal(0); }); - it('should not call onClose when closed', () => { + it('should not call onClose when closed', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - const { setProps } = render( + const { setProps } = await render( Message , @@ -199,9 +239,13 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration / 2); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); setProps({ open: false }); - clock.tick(autoHideDuration / 2); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); expect(handleClose.callCount).to.equal(0); }); @@ -215,12 +259,16 @@ describe('Joy ', () => { }, { type: 'keyboard', - enter: (container: HTMLElement) => act(() => container.querySelector('button')!.focus()), - leave: (container: HTMLElement) => act(() => container.querySelector('button')!.blur()), + enter: async (container: HTMLElement) => { + await act(async () => container.querySelector('button')!.focus()); + }, + leave: async (container: HTMLElement) => { + await act(async () => container.querySelector('button')!.blur()); + }, }, ].forEach((userInteraction) => { describe(`interacting with ${userInteraction.type}`, () => { - it('should be able to interrupt the timer', () => { + it('should be able to interrupt the timer', async () => { const handleMouseEnter = spy(); const handleMouseLeave = spy(); const handleBlur = spy(); @@ -228,7 +276,7 @@ describe('Joy ', () => { const handleClose = spy(); const autoHideDuration = 2e3; - const { container } = render( + const { container } = await render( undo} open @@ -245,8 +293,10 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration / 2); - userInteraction.enter(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); + await await userInteraction.enter(container.querySelector('div')!); if (userInteraction.type === 'keyboard') { expect(handleFocus.callCount).to.equal(1); @@ -254,8 +304,10 @@ describe('Joy ', () => { expect(handleMouseEnter.callCount).to.equal(1); } - clock.tick(autoHideDuration / 2); - userInteraction.leave(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); + await await userInteraction.leave(container.querySelector('div')!); if (userInteraction.type === 'keyboard') { expect(handleBlur.callCount).to.equal(1); @@ -264,18 +316,20 @@ describe('Joy ', () => { } expect(handleClose.callCount).to.equal(0); - clock.tick(2e3); + await act(async () => { + await timer?.tickAsync(2e3); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); }); - it('should not call onClose with not timeout after user interaction', () => { + it('should not call onClose with not timeout after user interaction', async () => { const handleClose = spy(); const autoHideDuration = 2e3; const resumeHideDuration = 3e3; - const { container } = render( + const { container } = await render( undo} open @@ -289,24 +343,30 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration / 2); - userInteraction.enter(container.querySelector('div')!); - clock.tick(autoHideDuration / 2); - userInteraction.leave(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); + await userInteraction.enter(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); + await userInteraction.leave(container.querySelector('div')!); expect(handleClose.callCount).to.equal(0); - clock.tick(2e3); + await act(async () => { + await timer?.tickAsync(2e3); + }); expect(handleClose.callCount).to.equal(0); }); - it('should call onClose when timer done after user interaction', () => { + it('should call onClose when timer done after user interaction', async () => { const handleClose = spy(); const autoHideDuration = 2e3; const resumeHideDuration = 3e3; - const { container } = render( + const { container } = await render( undo} open @@ -320,24 +380,30 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration / 2); - userInteraction.enter(container.querySelector('div')!); - clock.tick(autoHideDuration / 2); - userInteraction.leave(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); + await userInteraction.enter(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(autoHideDuration / 2); + }); + await userInteraction.leave(container.querySelector('div')!); expect(handleClose.callCount).to.equal(0); - clock.tick(resumeHideDuration); + await act(async () => { + await timer?.tickAsync(resumeHideDuration); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); }); - it('should call onClose immediately after user interaction when 0', () => { + it('should call onClose immediately after user interaction when 0', async () => { const handleClose = spy(); const autoHideDuration = 6e3; const resumeHideDuration = 0; - const { setProps, container } = render( + const { setProps, container } = await render( undo} open @@ -353,10 +419,14 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - userInteraction.enter(container.querySelector('div')!); - clock.tick(100); - userInteraction.leave(container.querySelector('div')!); - clock.tick(resumeHideDuration); + await userInteraction.enter(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(100); + }); + await userInteraction.leave(container.querySelector('div')!); + await act(async () => { + await timer?.tickAsync(resumeHideDuration); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); @@ -365,10 +435,10 @@ describe('Joy ', () => { }); describe('prop: disableWindowBlurListener', () => { - it('should pause auto hide when not disabled and window lost focus', () => { + it('should pause auto hide when not disabled and window lost focus', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - render( + await render( ', () => { , ); - act(() => { + await act(async () => { const bEvent = new window.Event('blur', { bubbles: false, cancelable: false, @@ -389,11 +459,13 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration); + await act(async () => { + await timer?.tickAsync(autoHideDuration); + }); expect(handleClose.callCount).to.equal(0); - act(() => { + await act(async () => { const fEvent = new window.Event('focus', { bubbles: false, cancelable: false, @@ -403,16 +475,18 @@ describe('Joy ', () => { expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration); + await act(async () => { + await timer?.tickAsync(autoHideDuration); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); }); - it('should not pause auto hide when disabled and window lost focus', () => { + it('should not pause auto hide when disabled and window lost focus', async () => { const handleClose = spy(); const autoHideDuration = 2e3; - render( + await render( ', () => { , ); - act(() => { + await act(async () => { const event = new window.Event('blur', { bubbles: false, cancelable: false }); window.dispatchEvent(event); }); expect(handleClose.callCount).to.equal(0); - clock.tick(autoHideDuration); + await act(async () => { + await timer?.tickAsync(autoHideDuration); + }); expect(handleClose.callCount).to.equal(1); expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); @@ -438,13 +514,13 @@ describe('Joy ', () => { }); describe('prop: open', () => { - it('should not render anything when closed', () => { - const { container } = render(Hello World!); + it('should not render anything when closed', async () => { + const { container } = await render(Hello World!); expect(container).to.have.text(''); }); - it('should be able show it after mounted', () => { - const { container, setProps } = render(Hello World!); + it('should be able show it after mounted', async () => { + const { container, setProps } = await render(Hello World!); expect(container).to.have.text(''); setProps({ open: true }); expect(container).to.have.text('Hello World!'); diff --git a/packages/mui-joy/src/Switch/Switch.test.tsx b/packages/mui-joy/src/Switch/Switch.test.tsx index d14a52403e2db0..357a4a6dc7187a 100644 --- a/packages/mui-joy/src/Switch/Switch.test.tsx +++ b/packages/mui-joy/src/Switch/Switch.test.tsx @@ -19,6 +19,7 @@ describe('', () => { { slotName: 'input', slotClassName: classes.input }, { slotName: 'thumb', slotClassName: classes.thumb }, ], + testVariantProps: { variant: 'soft' }, testCustomVariant: true, refInstanceof: window.HTMLDivElement, @@ -91,11 +92,11 @@ describe('', () => { expect(getByRole('switch')).to.have.property('readOnly', true); }); - it('the Checked state changes after change events', () => { + it('the Checked state changes after change events', async () => { const { getByRole } = render(); // how a user would trigger it - act(() => { + await act(async () => { getByRole('switch').click(); fireEvent.change(getByRole('switch'), { target: { checked: '' } }); }); @@ -116,7 +117,7 @@ describe('', () => { expect(getByText('bar')).toBeVisible(); }); - it('can receive startDecorator as function', () => { + it('can receive startDecorator as function', async () => { const { getByText, getByRole } = render( (checked ? 'On' : 'Off')} />, ); @@ -124,7 +125,7 @@ describe('', () => { expect(getByText('Off')).toBeVisible(); // how a user would trigger it - act(() => { + await act(async () => { getByRole('switch').click(); fireEvent.change(getByRole('switch'), { target: { checked: '' } }); }); @@ -132,7 +133,7 @@ describe('', () => { expect(getByText('On')).toBeVisible(); }); - it('can receive endDecorator as function', () => { + it('can receive endDecorator as function', async () => { const { getByText, getByRole } = render( (checked ? 'On' : 'Off')} />, ); @@ -140,7 +141,7 @@ describe('', () => { expect(getByText('Off')).toBeVisible(); // how a user would trigger it - act(() => { + await act(async () => { getByRole('switch').click(); fireEvent.change(getByRole('switch'), { target: { checked: '' } }); }); diff --git a/packages/mui-joy/src/Textarea/Textarea.test.tsx b/packages/mui-joy/src/Textarea/Textarea.test.tsx index 25f272d8b7939e..cae79a54ae9ad1 100644 --- a/packages/mui-joy/src/Textarea/Textarea.test.tsx +++ b/packages/mui-joy/src/Textarea/Textarea.test.tsx @@ -68,14 +68,14 @@ describe('Joy