Skip to content

Commit 6772d5d

Browse files
[code-infra] Fix StrictMode effects not being called twice in React 19 tests (#45812)
1 parent 0f46d19 commit 6772d5d

File tree

19 files changed

+96
-56
lines changed

19 files changed

+96
-56
lines changed

.mocharc.js

+3
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ module.exports = {
2020
'**/build/**',
2121
'docs/.next/**',
2222
],
23+
// detect-modules doesn't work with @babel/register
24+
// https://github.com/babel/babel/issues/6737
25+
'node-option': ['no-experimental-detect-module'],
2326
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
"@pigment-css/react": "0.0.30",
133133
"@playwright/test": "1.51.1",
134134
"@types/babel__core": "^7.20.5",
135+
"@types/babel__register": "^7.17.3",
135136
"@types/fs-extra": "^11.0.4",
136137
"@types/lodash": "^4.17.16",
137138
"@types/mocha": "^10.0.10",

packages-internal/test-utils/src/createRenderer.tsx

+18-18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
screen as rtlScreen,
1313
Screen,
1414
render as testingLibraryRender,
15+
RenderOptions as TestingLibraryRenderOptions,
1516
within,
1617
} from '@testing-library/react/pure';
1718
import { userEvent } from '@testing-library/user-event';
@@ -201,7 +202,7 @@ const customQueries = {
201202
findAllDescriptionsOf,
202203
};
203204

204-
interface RenderConfiguration {
205+
interface RenderConfiguration extends Pick<TestingLibraryRenderOptions, 'reactStrictMode'> {
205206
/**
206207
* https://testing-library.com/docs/react-testing-library/api#container
207208
*/
@@ -232,7 +233,7 @@ interface ServerRenderConfiguration extends RenderConfiguration {
232233
container: HTMLElement;
233234
}
234235

235-
export type RenderOptions = Partial<RenderConfiguration>;
236+
export type RenderOptions = Omit<Partial<RenderConfiguration>, 'reactStrictMode'>;
236237

237238
export interface MuiRenderResult extends RenderResult<typeof queries & typeof customQueries> {
238239
user: ReturnType<typeof userEvent.setup>;
@@ -256,14 +257,15 @@ function render(
256257
element: React.ReactElement<DataAttributes>,
257258
configuration: ClientRenderConfiguration,
258259
): MuiRenderResult {
259-
const { container, hydrate, wrapper } = configuration;
260+
const { container, hydrate, wrapper, reactStrictMode } = configuration;
260261

261262
const testingLibraryRenderResult = traceSync('render', () =>
262263
testingLibraryRender(element, {
263264
container,
264265
hydrate,
265266
queries: { ...queries, ...customQueries },
266267
wrapper,
268+
reactStrictMode,
267269
}),
268270
);
269271
const result: MuiRenderResult = {
@@ -628,24 +630,16 @@ export function createRenderer(globalOptions: CreateRendererOptions = {}): Rende
628630
serverContainer = null!;
629631
});
630632

631-
function createWrapper(options: Partial<RenderConfiguration>) {
632-
const {
633-
strict = globalStrict,
634-
strictEffects = globalStrictEffects,
635-
wrapper: InnerWrapper = React.Fragment,
636-
} = options;
633+
function createWrapper(options: Pick<RenderOptions, 'wrapper'>) {
634+
const { wrapper: InnerWrapper = React.Fragment } = options;
637635

638-
const usesLegacyRoot = reactMajor < 18;
639-
const Mode = strict && (strictEffects || usesLegacyRoot) ? React.StrictMode : React.Fragment;
640636
return function Wrapper({ children }: { children?: React.ReactNode }) {
641637
return (
642-
<Mode>
643-
<EmotionCacheProvider value={emotionCache}>
644-
<React.Profiler id={profiler.id} onRender={profiler.onRender}>
645-
<InnerWrapper>{children}</InnerWrapper>
646-
</React.Profiler>
647-
</EmotionCacheProvider>
648-
</Mode>
638+
<EmotionCacheProvider value={emotionCache}>
639+
<React.Profiler id={profiler.id} onRender={profiler.onRender}>
640+
<InnerWrapper>{children}</InnerWrapper>
641+
</React.Profiler>
642+
</EmotionCacheProvider>
649643
);
650644
};
651645
}
@@ -661,8 +655,14 @@ export function createRenderer(globalOptions: CreateRendererOptions = {}): Rende
661655
);
662656
}
663657

658+
const usesLegacyRoot = reactMajor < 18;
659+
const reactStrictMode =
660+
(options.strict ?? globalStrict) &&
661+
((options.strictEffects ?? globalStrictEffects) || usesLegacyRoot);
662+
664663
return render(element, {
665664
...options,
665+
reactStrictMode,
666666
hydrate: false,
667667
wrapper: createWrapper(options),
668668
});
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
require('@babel/register')({
2-
extensions: ['.js', '.mjs', '.ts', '.tsx'],
2+
extensions: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx'],
33
});

packages/mui-base/src/FocusTrap/FocusTrap.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import * as ReactDOM from 'react-dom';
33
import { expect } from 'chai';
4-
import { act, createRenderer, screen } from '@mui/internal-test-utils';
4+
import { act, createRenderer, reactMajor, screen } from '@mui/internal-test-utils';
55
import { FocusTrap } from '@mui/base/FocusTrap';
66
import { Portal } from '@mui/base/Portal';
77

@@ -219,7 +219,7 @@ describe('<FocusTrap />', () => {
219219
</div>
220220
);
221221
}
222-
const { setProps, getByRole } = render(<Test />);
222+
const { setProps, getByRole } = render(<Test />, { strict: reactMajor <= 18 });
223223
expect(screen.getByTestId('root')).toHaveFocus();
224224

225225
act(() => {

packages/mui-base/src/Input/Input.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import PropTypes from 'prop-types';
3-
import { createRenderer, fireEvent, screen, act } from '@mui/internal-test-utils';
3+
import { createRenderer, fireEvent, screen, act, reactMajor } from '@mui/internal-test-utils';
44
import { expect } from 'chai';
55
import { spy } from 'sinon';
66
import { Input, inputClasses, InputOwnerState } from '@mui/base/Input';
@@ -281,8 +281,8 @@ describe('<Input />', () => {
281281
);
282282
}).toErrorDev([
283283
'MUI: You have provided a `slots.input` to the input component\nthat does not correctly handle the `ref` prop.\nMake sure the `ref` prop is called with a HTMLInputElement.',
284-
// React 18 Strict Effects run mount effects twice
285-
React.version.startsWith('18') &&
284+
// React Strict Mode runs mount effects twice
285+
reactMajor >= 18 &&
286286
'MUI: You have provided a `slots.input` to the input component\nthat does not correctly handle the `ref` prop.\nMake sure the `ref` prop is called with a HTMLInputElement.',
287287
]);
288288
});

packages/mui-base/src/Portal/Portal.test.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { expect } from 'chai';
33
import { spy } from 'sinon';
4-
import { createRenderer } from '@mui/internal-test-utils';
4+
import { createRenderer, reactMajor } from '@mui/internal-test-utils';
55
import { Portal, PortalProps } from '@mui/base/Portal';
66

77
describe('<Portal />', () => {
@@ -44,6 +44,7 @@ describe('<Portal />', () => {
4444
<Portal disablePortal ref={refSpy}>
4545
<h1 className="woofPortal">Foo</h1>
4646
</Portal>,
47+
{ strict: reactMajor <= 18 },
4748
);
4849
const mountNode = document.querySelector('.woofPortal');
4950
expect(refSpy.args).to.deep.equal([[mountNode]]);
@@ -57,6 +58,7 @@ describe('<Portal />', () => {
5758
<Portal disablePortal ref={refSpy}>
5859
<h1 className="woofPortal">Foo</h1>
5960
</Portal>,
61+
{ strict: reactMajor <= 18 },
6062
);
6163
const mountNode = document.querySelector('.woofPortal');
6264
expect(refSpy.args).to.deep.equal([[mountNode]]);

packages/mui-base/src/useAutocomplete/useAutocomplete.test.js

+8
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ describe('useAutocomplete', () => {
313313
aboveErrorTestComponentMessage,
314314
aboveErrorTestComponentMessage,
315315
],
316+
19: [
317+
muiErrorMessage,
318+
muiErrorMessage,
319+
nodeErrorMessage,
320+
nodeErrorMessage,
321+
nodeErrorMessage,
322+
nodeErrorMessage,
323+
],
316324
};
317325

318326
const devErrorMessages = errorMessagesByReactMajor[reactMajor] || defaultErrorMessages;

packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -1931,7 +1931,8 @@ describe('Joy <Autocomplete />', () => {
19311931

19321932
await user.click(screen.getByText('Reset'));
19331933

1934-
const expectedCallCount = reactMajor === 18 ? 4 : 2;
1934+
// eslint-disable-next-line no-nested-ternary
1935+
const expectedCallCount = reactMajor >= 19 ? 3 : reactMajor === 18 ? 4 : 2;
19351936

19361937
expect(handleInputChange.callCount).to.equal(expectedCallCount);
19371938
expect(handleInputChange.args[expectedCallCount - 1][1]).to.equal(options[1].name);
@@ -2209,7 +2210,8 @@ describe('Joy <Autocomplete />', () => {
22092210

22102211
expect(handleHighlightChange.callCount).to.equal(
22112212
// FIXME: highlighted index implementation should be implemented using React not the DOM.
2212-
reactMajor >= 18 ? 4 : 3,
2213+
// eslint-disable-next-line no-nested-ternary
2214+
reactMajor >= 19 ? 5 : reactMajor >= 18 ? 4 : 3,
22132215
);
22142216
if (reactMajor >= 18) {
22152217
expect(handleHighlightChange.args[2][0]).to.equal(undefined);
@@ -2223,7 +2225,8 @@ describe('Joy <Autocomplete />', () => {
22232225
fireEvent.keyDown(textbox, { key: 'ArrowDown' });
22242226
expect(handleHighlightChange.callCount).to.equal(
22252227
// FIXME: highlighted index implementation should be implemented using React not the DOM.
2226-
reactMajor >= 18 ? 5 : 4,
2228+
// eslint-disable-next-line no-nested-ternary
2229+
reactMajor >= 19 ? 6 : reactMajor >= 18 ? 5 : 4,
22272230
);
22282231
expect(handleHighlightChange.lastCall.args[0]).not.to.equal(undefined);
22292232
expect(handleHighlightChange.lastCall.args[1]).to.equal(options[1]);
@@ -2240,7 +2243,8 @@ describe('Joy <Autocomplete />', () => {
22402243
fireEvent.mouseMove(firstOption);
22412244
expect(handleHighlightChange.callCount).to.equal(
22422245
// FIXME: highlighted index implementation should be implemented using React not the DOM.
2243-
reactMajor >= 18 ? 4 : 3,
2246+
// eslint-disable-next-line no-nested-ternary
2247+
reactMajor >= 19 ? 5 : reactMajor >= 18 ? 4 : 3,
22442248
);
22452249
if (reactMajor >= 18) {
22462250
expect(handleHighlightChange.args[2][0]).to.equal(undefined);

packages/mui-material/src/InputBase/InputBase.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ describe('<InputBase />', () => {
282282

283283
let expectedOccurrences = 1;
284284

285-
if (reactMajor === 18) {
285+
if (reactMajor >= 18) {
286286
expectedOccurrences = 2;
287287
}
288288

@@ -507,7 +507,7 @@ describe('<InputBase />', () => {
507507

508508
let expectedOccurrences = 1;
509509

510-
if (reactMajor === 18) {
510+
if (reactMajor >= 18) {
511511
expectedOccurrences = 2;
512512
}
513513
expect(() => {

packages/mui-material/src/Portal/Portal.test.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { expect } from 'chai';
33
import { spy } from 'sinon';
4-
import { createRenderer } from '@mui/internal-test-utils';
4+
import { createRenderer, reactMajor } from '@mui/internal-test-utils';
55
import Portal, { PortalProps } from '@mui/material/Portal';
66

77
describe('<Portal />', () => {
@@ -44,6 +44,7 @@ describe('<Portal />', () => {
4444
<Portal disablePortal ref={refSpy}>
4545
<h1 className="woofPortal">Foo</h1>
4646
</Portal>,
47+
{ strict: reactMajor <= 18 },
4748
);
4849
const mountNode = document.querySelector('.woofPortal');
4950
expect(refSpy.args).to.deep.equal([[mountNode]]);
@@ -57,6 +58,7 @@ describe('<Portal />', () => {
5758
<Portal disablePortal ref={refSpy}>
5859
<h1 className="woofPortal">Foo</h1>
5960
</Portal>,
61+
{ strict: reactMajor <= 18 },
6062
);
6163
const mountNode = document.querySelector('.woofPortal');
6264
expect(refSpy.args).to.deep.equal([[mountNode]]);

packages/mui-material/src/Select/Select.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ describe('<Select />', () => {
380380

381381
let expectedOccurrences = 2;
382382

383-
if (reactMajor === 18) {
383+
if (reactMajor >= 18) {
384384
expectedOccurrences = 3;
385385
}
386386

packages/mui-material/src/Tabs/Tabs.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,11 @@ describe('<Tabs />', () => {
372372
);
373373
}).toErrorDev([
374374
'You can provide one of the following values: 1, 3',
375-
// React 18 Strict Effects run mount effects twice
376-
reactMajor === 18 && 'You can provide one of the following values: 1, 3',
375+
// React Strict Mode runs mount effects twice
376+
reactMajor >= 18 && 'You can provide one of the following values: 1, 3',
377377
'You can provide one of the following values: 1, 3',
378-
// React 18 Strict Effects run mount effects twice
379-
reactMajor === 18 && 'You can provide one of the following values: 1, 3',
378+
// React Strict Mode runs mount effects twice
379+
reactMajor >= 18 && 'You can provide one of the following values: 1, 3',
380380
'You can provide one of the following values: 1, 3',
381381
'You can provide one of the following values: 1, 3',
382382
]);

packages/mui-material/src/Unstable_TrapFocus/FocusTrap.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import * as ReactDOM from 'react-dom';
33
import { expect } from 'chai';
4-
import { act, createRenderer, screen } from '@mui/internal-test-utils';
4+
import { act, createRenderer, reactMajor, screen } from '@mui/internal-test-utils';
55
import FocusTrap from '@mui/material/Unstable_TrapFocus';
66
import Portal from '@mui/material/Portal';
77

@@ -219,7 +219,7 @@ describe('<FocusTrap />', () => {
219219
</div>
220220
);
221221
}
222-
const { setProps, getByRole } = render(<Test />);
222+
const { setProps, getByRole } = render(<Test />, { strict: reactMajor <= 18 });
223223
expect(screen.getByTestId('root')).toHaveFocus();
224224

225225
act(() => {

packages/mui-material/src/styles/ThemeProviderWithVars.test.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { expect } from 'chai';
3-
import { createRenderer, screen, fireEvent } from '@mui/internal-test-utils';
3+
import { createRenderer, screen, fireEvent, reactMajor } from '@mui/internal-test-utils';
44
import Box from '@mui/material/Box';
55
import {
66
CssVarsProvider,
@@ -470,11 +470,11 @@ describe('[Material UI] ThemeProviderWithVars', () => {
470470
}
471471
const { container } = render(<App />);
472472

473-
expect(container).to.have.text('1 light');
473+
expect(container).to.have.text(`${reactMajor >= 19 ? 2 : 1} light`);
474474

475475
fireEvent.click(screen.getByRole('button'));
476476

477-
expect(container).to.have.text('1 light');
477+
expect(container).to.have.text(`${reactMajor >= 19 ? 2 : 1} light`);
478478
});
479479

480480
it('palette mode should change if not using CSS variables', () => {
@@ -505,12 +505,14 @@ describe('[Material UI] ThemeProviderWithVars', () => {
505505
}
506506
const { container } = render(<App />);
507507

508-
expect(container).to.have.text(`1 light ${createTheme().palette.primary.main}`);
508+
expect(container).to.have.text(
509+
`${reactMajor >= 19 ? 2 : 1} light ${createTheme().palette.primary.main}`,
510+
);
509511

510512
fireEvent.click(screen.getByRole('button'));
511513

512514
expect(container).to.have.text(
513-
`2 dark ${createTheme({ palette: { mode: 'dark' } }).palette.primary.main}`,
515+
`${reactMajor >= 19 ? 3 : 2} dark ${createTheme({ palette: { mode: 'dark' } }).palette.primary.main}`,
514516
);
515517
});
516518

@@ -542,10 +544,10 @@ describe('[Material UI] ThemeProviderWithVars', () => {
542544
}
543545
const { container } = render(<App />);
544546

545-
expect(container).to.have.text('1 light');
547+
expect(container).to.have.text(`${reactMajor >= 19 ? 2 : 1} light`);
546548

547549
fireEvent.click(screen.getByRole('button'));
548550

549-
expect(container).to.have.text('2 dark');
551+
expect(container).to.have.text(`${reactMajor >= 19 ? 3 : 2} dark`);
550552
});
551553
});

packages/mui-material/src/useAutocomplete/useAutocomplete.test.js

+8
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ describe('useAutocomplete', () => {
313313
aboveErrorTestComponentMessage,
314314
aboveErrorTestComponentMessage,
315315
],
316+
19: [
317+
muiErrorMessage,
318+
muiErrorMessage,
319+
nodeErrorMessage,
320+
nodeErrorMessage,
321+
nodeErrorMessage,
322+
nodeErrorMessage,
323+
],
316324
};
317325

318326
const devErrorMessages = errorMessagesByReactMajor[reactMajor] || defaultErrorMessages;

packages/mui-system/src/cssVars/useCurrentColorScheme.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { expect } from 'chai';
33
import { spy } from 'sinon';
4-
import { createRenderer, fireEvent, act, screen, reactMajor } from '@mui/internal-test-utils';
4+
import { createRenderer, fireEvent, act, screen } from '@mui/internal-test-utils';
55
import {
66
DEFAULT_MODE_STORAGE_KEY,
77
DEFAULT_COLOR_SCHEME_STORAGE_KEY,
@@ -111,7 +111,7 @@ describe('useCurrentColorScheme', () => {
111111
const { container } = render(<Data />);
112112

113113
expect(container.firstChild.textContent).to.equal('light');
114-
expect(effectRunCount).to.equal(reactMajor >= 19 ? 2 : 3);
114+
expect(effectRunCount).to.equal(3);
115115
});
116116

117117
it('[noSsr] does not trigger a re-render', () => {

0 commit comments

Comments
 (0)