Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[code-infra] Bring some test updates
  • Loading branch information
Janpot committed Nov 19, 2025
commit 7f9bf1d564f1fe92f1e10978438bdbba0db16124
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ docs/public/static/blog/feed/*
.nx/workspace-data
screenshots
packed
test-results
.env

# typescript
*.tsbuildinfo
Expand Down
2 changes: 1 addition & 1 deletion .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
],
recursive: true,
timeout: (process.env.CIRCLECI === 'true' ? 5 : 2) * 1000, // Circle CI has low-performance CPUs.
reporter: 'dot',
reporter: 'spec',
require: ['@mui/internal-test-utils/setupBabel', '@mui/internal-test-utils/setupJSDOM'],
'watch-ignore': [
// default
Expand Down
2 changes: 1 addition & 1 deletion docs/src/theming.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen, fireEvent } from '@mui/internal-test-utils';
import { createRenderer, fireEvent, screen } from '@mui/internal-test-utils';
import { ThemeProvider, createTheme, useColorScheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import { THEME_ID as JOY_THEME_ID, extendTheme } from '@mui/joy/styles';
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@
"@babel/plugin-transform-react-constant-elements": "^7.27.1",
"@mui-internal/api-docs-builder": "workspace:^",
"@mui-internal/api-docs-builder-core": "workspace:^",
"@mui/internal-bundle-size-checker": "^1.0.9-canary.55",
"@mui/internal-babel-plugin-minify-errors": "^2.0.8-canary.11",
"@mui/internal-bundle-size-checker": "^1.0.9-canary.55",
"@mui/internal-code-infra": "^0.0.3-canary.50",
"@mui/internal-docs-utils": "workspace:^",
"@mui/internal-netlify-cache": "^0.0.2-canary.1",
Expand All @@ -116,8 +116,8 @@
"@next/eslint-plugin-next": "^15.5.6",
"@octokit/rest": "^22.0.1",
"@pigment-css/react": "0.0.30",
"@pnpm/find-workspace-dir": "^1000.1.3",
"@playwright/test": "1.56.1",
"@pnpm/find-workspace-dir": "^1000.1.3",
"@types/babel__core": "^7.20.5",
"@types/babel__register": "^7.17.3",
"@types/mocha": "^10.0.10",
Expand Down
1 change: 1 addition & 0 deletions packages-internal/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@types/chai": "^4.3.20",
"@types/chai-dom": "^1.11.3",
"@types/format-util": "^1.0.4",
"@types/jsdom": "^21.1.7",
"@types/prop-types": "^15.7.15",
"@types/react": "^19.2.4",
"@types/react-dom": "^19.2.3",
Expand Down
95 changes: 80 additions & 15 deletions packages-internal/test-utils/src/createRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { userEvent } from '@testing-library/user-event';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { useFakeTimers } from 'sinon';
import { VitestUtils } from 'vitest';
import reactMajor from './reactMajor';

function queryAllDescriptionsOf(baseElement: HTMLElement, element: Element): HTMLElement[] {
Expand Down Expand Up @@ -200,36 +201,76 @@ function createVitestClock(
defaultMode: 'fake' | 'real',
config: ClockConfig,
options: Exclude<Parameters<typeof useFakeTimers>[0], number | Date>,
vi: any,
vi: import('vitest').VitestUtils,
): Clock {
if (defaultMode === 'fake') {
beforeEach(() => {
vi.useFakeTimers(options);
vi.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,
toFake: [
'setTimeout',
'setInterval',
'clearTimeout',
'clearInterval',
'requestAnimationFrame',
'cancelAnimationFrame',
'performance',
'Date',
],
...options,
});
if (config) {
vi.setSystemTime(config);
}
});
afterEach(() => {
vi.useRealTimers();
});
} else {
beforeEach(() => {
if (config) {
vi.setSystemTime(config);
}
});
afterEach(() => {
vi.useRealTimers();
});
}

afterEach(async () => {
if (vi.isFakeTimers()) {
await rtlAct(async () => {
vi.runOnlyPendingTimers();
});
vi.useRealTimers();
}
});

return {
withFakeTimers: () => {
if (vi.isFakeTimers()) {
return;
}
beforeEach(() => {
vi.useFakeTimers(options);
});
afterEach(() => {
vi.useRealTimers();
vi.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 we did for the past years when using `sinon` < 8.
shouldClearNativeTimers: true,
toFake: [
'setTimeout',
'setInterval',
'clearTimeout',
'clearInterval',
'requestAnimationFrame',
'cancelAnimationFrame',
'performance',
'Date',
],
...options,
});
if (config) {
vi.setSystemTime(config);
}
});
},
runToLast: () => {
Expand Down Expand Up @@ -351,7 +392,7 @@ export interface CreateRendererOptions extends Pick<RenderOptions, 'strict' | 's
* Vitest needs to be injected because this file is transpiled to commonjs and vitest is an esm module.
* @default {}
*/
vi?: any;
vi?: VitestUtils;
}

export function createRenderer(globalOptions: CreateRendererOptions = {}): Renderer {
Expand All @@ -360,7 +401,7 @@ export function createRenderer(globalOptions: CreateRendererOptions = {}): Rende
clockConfig,
strict: globalStrict = true,
strictEffects: globalStrictEffects = globalStrict,
vi = (globalThis as any).vi,
vi = (globalThis as any).vi as typeof import('vitest').vi | undefined,
clockOptions,
} = globalOptions;
// save stack to re-use in test-hooks
Expand Down Expand Up @@ -615,6 +656,30 @@ function act<T>(callback: () => void | T | Promise<T>) {

const bodyBoundQueries = within(document.body, { ...queries, ...customQueries });

export * from '@testing-library/react/pure';
export { renderHook, waitFor, within } from '@testing-library/react/pure';
export { act, fireEvent };
export const screen: Screen & typeof bodyBoundQueries = { ...rtlScreen, ...bodyBoundQueries };

export async function flushEffects(): Promise<void> {
await act(async () => {});
}

/**
* returns true when touch is suported and can be mocked
*/
export function supportsTouch() {
// only run in supported browsers
if (typeof Touch === 'undefined') {
return false;
}

try {
// eslint-disable-next-line no-new
new Touch({ identifier: 0, target: window, pageX: 0, pageY: 0 });
} catch {
// Touch constructor not supported
return false;
}

return true;
}
4 changes: 2 additions & 2 deletions packages/mui-joy/src/MenuItem/MenuItem.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { act, createRenderer, fireEvent, screen } from '@mui/internal-test-utils';
import { act, createRenderer, fireEvent, screen, supportsTouch } from '@mui/internal-test-utils';
import { MenuProvider, MenuProviderValue } from '@mui/base/useMenu';
import { ThemeProvider } from '@mui/joy/styles';
import MenuItem, { menuItemClasses as classes } from '@mui/joy/MenuItem';
Expand Down Expand Up @@ -142,7 +142,7 @@ describe('Joy <MenuItem />', () => {

it('should fire onTouchStart', function touchStartTest() {
// only run in supported browsers
if (typeof Touch === 'undefined') {
if (!supportsTouch()) {
this.skip();
}

Expand Down
3 changes: 2 additions & 1 deletion packages/mui-material/src/ButtonBase/ButtonBase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
focusVisible,
simulatePointerDevice,
programmaticFocusTriggersFocusVisible,
supportsTouch,
} from '@mui/internal-test-utils';
import describeSkipIf from '@mui/internal-test-utils/describeSkipIf';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -199,7 +200,7 @@ describe('<ButtonBase />', () => {
const button = screen.getByText('Hello');

// only run in supported browsers
if (typeof Touch !== 'undefined') {
if (supportsTouch()) {
const touch = new Touch({ identifier: 0, target: button, clientX: 0, clientY: 0 });

fireEvent.touchStart(button, { touches: [touch] });
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-material/src/ButtonBase/TouchRipple.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ const TouchRipple = React.forwardRef(function TouchRipple(inProps, ref) {
rippleSize = Math.sqrt(sizeX ** 2 + sizeY ** 2);
}

// Touche devices
// Touch devices
if (event?.touches) {
// check that this isn't another touchstart due to multitouch
// otherwise we will only clear a single timer when unmounting while two
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-material/src/ButtonBase/TouchRipple.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ describe('<TouchRipple />', () => {
it('should handle empty event.touches', () => {
const { instance } = renderTouchRipple();

expect(() => instance.start({ type: 'touchstart', touches: [] })).not.toErrorDev();
instance.start({ type: 'touchstart', touches: [], clientX: 0, clientY: 0 });
});
});
});
32 changes: 16 additions & 16 deletions packages/mui-material/src/FormControl/FormControl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('<FormControl />', () => {
});

describe('prop: disabled', () => {
it('will be unfocused if it gets disabled', () => {
it('will be unfocused if it gets disabled', async () => {
const readContext = spy();
const { container, setProps } = render(
<FormControl>
Expand All @@ -94,7 +94,7 @@ describe('<FormControl />', () => {
);
expect(readContext.args[0][0]).to.have.property('focused', false);

act(() => {
await act(async () => {
container.querySelector('input').focus();
});
expect(readContext.lastCall.args[0]).to.have.property('focused', true);
Expand Down Expand Up @@ -271,19 +271,19 @@ describe('<FormControl />', () => {

describe('callbacks', () => {
describe('onFilled', () => {
it('should set the filled state', () => {
it('should set the filled state', async () => {
const formControlRef = React.createRef();
render(<FormControlled ref={formControlRef} />);

expect(formControlRef.current).to.have.property('filled', false);

act(() => {
await act(async () => {
formControlRef.current.onFilled();
});

expect(formControlRef.current).to.have.property('filled', true);

act(() => {
await act(async () => {
formControlRef.current.onFilled();
});

Expand All @@ -292,23 +292,23 @@ describe('<FormControl />', () => {
});

describe('onEmpty', () => {
it('should clean the filled state', () => {
it('should clean the filled state', async () => {
const formControlRef = React.createRef();
render(<FormControlled ref={formControlRef} />);

act(() => {
await act(async () => {
formControlRef.current.onFilled();
});

expect(formControlRef.current).to.have.property('filled', true);

act(() => {
await act(async () => {
formControlRef.current.onEmpty();
});

expect(formControlRef.current).to.have.property('filled', false);

act(() => {
await act(async () => {
formControlRef.current.onEmpty();
});

Expand All @@ -317,18 +317,18 @@ describe('<FormControl />', () => {
});

describe('handleFocus', () => {
it('should set the focused state', () => {
it('should set the focused state', async () => {
const formControlRef = React.createRef();
render(<FormControlled ref={formControlRef} />);
expect(formControlRef.current).to.have.property('focused', false);

act(() => {
await act(async () => {
formControlRef.current.onFocus();
});

expect(formControlRef.current).to.have.property('focused', true);

act(() => {
await act(async () => {
formControlRef.current.onFocus();
});

Expand All @@ -337,24 +337,24 @@ describe('<FormControl />', () => {
});

describe('handleBlur', () => {
it('should clear the focused state', () => {
it('should clear the focused state', async () => {
const formControlRef = React.createRef();
render(<FormControlled ref={formControlRef} />);
expect(formControlRef.current).to.have.property('focused', false);

act(() => {
await act(async () => {
formControlRef.current.onFocus();
});

expect(formControlRef.current).to.have.property('focused', true);

act(() => {
await act(async () => {
formControlRef.current.onBlur();
});

expect(formControlRef.current).to.have.property('focused', false);

act(() => {
await act(async () => {
formControlRef.current.onBlur();
});

Expand Down
4 changes: 2 additions & 2 deletions packages/mui-material/src/MenuItem/MenuItem.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import { spy } from 'sinon';
import { act, createRenderer, fireEvent, screen } from '@mui/internal-test-utils';
import { act, createRenderer, fireEvent, screen, supportsTouch } from '@mui/internal-test-utils';
import MenuItem, { menuItemClasses as classes } from '@mui/material/MenuItem';
import ButtonBase from '@mui/material/ButtonBase';
import ListContext from '../List/ListContext';
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('<MenuItem />', () => {

it('should fire onTouchStart', function touchStartTest() {
// only run in supported browsers
if (typeof Touch === 'undefined') {
if (!supportsTouch()) {
this.skip();
}

Expand Down
Loading
Loading