Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as React from 'react';
import { __unsafe_useEmotionCache } from '@emotion/react';
import { StyledEngineProvider, GlobalStyles } from '@mui/styled-engine';
import { createRenderer } from '@mui/internal-test-utils';
import styled, { StyledEngineProvider, GlobalStyles } from '@mui/styled-engine';
import { createRenderer, isJsdom } from '@mui/internal-test-utils';
import { expect } from 'chai';
import { TEST_INTERNALS_DO_NOT_USE } from './StyledEngineProvider';

const isJSDOM = isJsdom();

describe('[Emotion] StyledEngineProvider', () => {
const { render } = createRenderer();

Expand Down Expand Up @@ -84,3 +87,51 @@ describe('[Emotion] StyledEngineProvider', () => {
expect(innerCache).to.equal(upperCache);
});
});

// In its own `describe` so the `TEST_INTERNALS_DO_NOT_USE.insert` hook above is
// NOT installed — that hook intercepts emotion's insert and never applies the
// CSS, which would prevent these tests from observing real cascade resolution.
describe('[Emotion] StyledEngineProvider cascade layers', () => {
const { render } = createRenderer();

// Regression test for the visual-regression flake where MUI's own layered
// styles were intermittently defeated (e.g. CustomizedSwitches: the Stack
// `margin-left: 8px` spacing collapsed to 0, reverting to the pre-cssLayer
// look).
//
// `injectFirst` (UNLAYERED) and `enableCssLayer` (inside `@layer mui`) both
// used cache key `css`, so the same style hashed to the same class name in
// both. An unlayered rule beats every cascade layer, so the unlayered copy
// overrode MUI's layered style. A dedicated key for the layered cache keeps
// the generated class names distinct, so the collision cannot happen.
//
// Asserts the actual cascade ORDERING via `getComputedStyle`, so it needs a
// real browser: jsdom does not resolve `@layer` precedence.
it.skipIf(isJSDOM)(
'layered styles are not defeated by identically-named unlayered styles',
() => {
const Child = styled('span')({ marginLeft: 0 }); // reset, low specificity
const Parent = styled('div')({ '& > span': { marginLeft: '8px' } }); // spacing, higher specificity

const { container } = render(
<React.Fragment>
{/* UNLAYERED: inserts `.css-<hash> { margin-left: 0 }` */}
<StyledEngineProvider injectFirst>
<Child />
</StyledEngineProvider>
{/* layered: the parent's higher-specificity spacing must still win */}
<StyledEngineProvider enableCssLayer>
<Parent>
<Child data-testid="child" />
</Parent>
</StyledEngineProvider>
</React.Fragment>,
);

const child = container.querySelector('[data-testid="child"]') as HTMLElement;
// Without a dedicated layered key the unlayered `.css-<hash>{margin-left:0}`
// collides with and defeats the layered spacing, collapsing this to `0px`.
expect(getComputedStyle(child).marginLeft).to.equal('8px');
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ function getCache(injectFirst?: boolean, enableCssLayer?: boolean) {
}
const emotionCache = createEmotionCache(
{
key: 'css',
// Use a dedicated key for the cascade-layer cache so its generated class
// names (e.g. `mui-fyswvn`) can never collide with identically-hashed
// UNLAYERED classes from the default/`injectFirst` cache (key `css`).
// A collision lets an unlayered `.css-xxx` rule beat the layered one,
// defeating MUI's own styles.
key: enableCssLayer ? 'mui' : 'css',
insertionPoint: injectFirst ? insertionPoint : undefined,
},
MyStyleSheet,
Expand Down
Loading