@mui/internal-test-utils 1.0.0-dev.20240529-082515-213b5e33ab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/build/.tsbuildinfo +1 -0
- package/build/KarmaReporterReactProfiler.d.ts +51 -0
- package/build/KarmaReporterReactProfiler.d.ts.map +1 -0
- package/build/KarmaReporterReactProfiler.js +68 -0
- package/build/KarmaReporterReactProfiler.js.map +1 -0
- package/build/components.d.ts +36 -0
- package/build/components.d.ts.map +1 -0
- package/build/components.js +78 -0
- package/build/components.js.map +1 -0
- package/build/createDOM.d.ts +3 -0
- package/build/createDOM.d.ts.map +1 -0
- package/build/createDOM.js +60 -0
- package/build/createDOM.js.map +1 -0
- package/build/createDescribe.d.ts +8 -0
- package/build/createDescribe.d.ts.map +1 -0
- package/build/createDescribe.js +22 -0
- package/build/createDescribe.js.map +1 -0
- package/build/createMount.d.ts +9 -0
- package/build/createMount.d.ts.map +1 -0
- package/build/createMount.js +120 -0
- package/build/createMount.js.map +1 -0
- package/build/createRenderer.d.ts +231 -0
- package/build/createRenderer.d.ts.map +1 -0
- package/build/createRenderer.js +506 -0
- package/build/createRenderer.js.map +1 -0
- package/build/createRenderer.test.d.ts +2 -0
- package/build/createRenderer.test.d.ts.map +1 -0
- package/build/createRenderer.test.js +48 -0
- package/build/createRenderer.test.js.map +1 -0
- package/build/describeConformance.d.ts +152 -0
- package/build/describeConformance.d.ts.map +1 -0
- package/build/describeConformance.js +815 -0
- package/build/describeConformance.js.map +1 -0
- package/build/findOutermostIntrinsic.d.ts +14 -0
- package/build/findOutermostIntrinsic.d.ts.map +1 -0
- package/build/findOutermostIntrinsic.js +22 -0
- package/build/findOutermostIntrinsic.js.map +1 -0
- package/build/findOutermostIntrinsic.test.d.ts +2 -0
- package/build/findOutermostIntrinsic.test.d.ts.map +1 -0
- package/build/findOutermostIntrinsic.test.js +70 -0
- package/build/findOutermostIntrinsic.test.js.map +1 -0
- package/build/fireDiscreteEvent.d.ts +7 -0
- package/build/fireDiscreteEvent.d.ts.map +1 -0
- package/build/fireDiscreteEvent.js +98 -0
- package/build/fireDiscreteEvent.js.map +1 -0
- package/build/flushMicrotasks.d.ts +2 -0
- package/build/flushMicrotasks.d.ts.map +1 -0
- package/build/flushMicrotasks.js +11 -0
- package/build/flushMicrotasks.js.map +1 -0
- package/build/focusVisible.d.ts +7 -0
- package/build/focusVisible.d.ts.map +1 -0
- package/build/focusVisible.js +33 -0
- package/build/focusVisible.js.map +1 -0
- package/build/index.d.ts +18 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +59 -0
- package/build/index.js.map +1 -0
- package/build/init.d.ts +2 -0
- package/build/init.d.ts.map +1 -0
- package/build/init.js +42 -0
- package/build/init.js.map +1 -0
- package/build/initMatchers.d.ts +81 -0
- package/build/initMatchers.d.ts.map +1 -0
- package/build/initMatchers.js +431 -0
- package/build/initMatchers.js.map +1 -0
- package/build/initMatchers.test.d.ts +2 -0
- package/build/initMatchers.test.d.ts.map +1 -0
- package/build/initMatchers.test.js +103 -0
- package/build/initMatchers.test.js.map +1 -0
- package/build/initPlaywrightMatchers.d.ts +25 -0
- package/build/initPlaywrightMatchers.d.ts.map +1 -0
- package/build/initPlaywrightMatchers.js +63 -0
- package/build/initPlaywrightMatchers.js.map +1 -0
- package/build/mochaHooks.d.ts +25 -0
- package/build/mochaHooks.d.ts.map +1 -0
- package/build/mochaHooks.js +171 -0
- package/build/mochaHooks.js.map +1 -0
- package/build/mochaHooks.test.d.ts +2 -0
- package/build/mochaHooks.test.d.ts.map +1 -0
- package/build/mochaHooks.test.js +112 -0
- package/build/mochaHooks.test.js.map +1 -0
- package/build/setup.d.ts +2 -0
- package/build/setup.d.ts.map +1 -0
- package/build/setup.js +10 -0
- package/build/setup.js.map +1 -0
- package/build/setupBabel.d.ts +2 -0
- package/build/setupBabel.d.ts.map +1 -0
- package/build/setupBabel.js +5 -0
- package/build/setupBabel.js.map +1 -0
- package/build/setupBabelPlaywright.d.ts +2 -0
- package/build/setupBabelPlaywright.d.ts.map +1 -0
- package/build/setupBabelPlaywright.js +11 -0
- package/build/setupBabelPlaywright.js.map +1 -0
- package/build/setupJSDOM.d.ts +7 -0
- package/build/setupJSDOM.d.ts.map +1 -0
- package/build/setupJSDOM.js +17 -0
- package/build/setupJSDOM.js.map +1 -0
- package/build/setupKarma.d.ts +2 -0
- package/build/setupKarma.d.ts.map +1 -0
- package/build/setupKarma.js +56 -0
- package/build/setupKarma.js.map +1 -0
- package/build/until.d.ts +2 -0
- package/build/until.d.ts.map +1 -0
- package/build/until.js +26 -0
- package/build/until.js.map +1 -0
- package/build/until.test.d.ts +2 -0
- package/build/until.test.d.ts.map +1 -0
- package/build/until.test.js +118 -0
- package/build/until.test.js.map +1 -0
- package/build/userEvent.d.ts +8 -0
- package/build/userEvent.d.ts.map +1 -0
- package/build/userEvent.js +61 -0
- package/build/userEvent.js.map +1 -0
- package/package.json +76 -0
- package/src/KarmaReporterReactProfiler.js +81 -0
- package/src/components.tsx +61 -0
- package/src/createDOM.d.ts +9 -0
- package/src/createDOM.js +67 -0
- package/src/createDescribe.ts +31 -0
- package/src/createMount.tsx +136 -0
- package/src/createRenderer.test.js +31 -0
- package/src/createRenderer.tsx +737 -0
- package/src/describeConformance.tsx +1117 -0
- package/src/findOutermostIntrinsic.test.js +68 -0
- package/src/findOutermostIntrinsic.ts +19 -0
- package/src/fireDiscreteEvent.ts +76 -0
- package/src/flushMicrotasks.ts +8 -0
- package/src/focusVisible.ts +28 -0
- package/src/index.ts +24 -0
- package/src/init.js +15 -0
- package/src/initMatchers.test.js +126 -0
- package/src/initMatchers.ts +658 -0
- package/src/initPlaywrightMatchers.ts +96 -0
- package/src/mochaHooks.js +209 -0
- package/src/mochaHooks.test.js +113 -0
- package/src/setup.js +10 -0
- package/src/setupBabel.js +3 -0
- package/src/setupBabelPlaywright.js +10 -0
- package/src/setupJSDOM.js +20 -0
- package/src/setupKarma.js +65 -0
- package/src/until.js +28 -0
- package/src/until.test.js +120 -0
- package/src/userEvent.ts +35 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
/* eslint-env mocha */
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import { ReactWrapper } from 'enzyme';
|
|
5
|
+
import ReactTestRenderer from 'react-test-renderer';
|
|
6
|
+
import createMount from './createMount';
|
|
7
|
+
import createDescribe from './createDescribe';
|
|
8
|
+
import findOutermostIntrinsic from './findOutermostIntrinsic';
|
|
9
|
+
import { MuiRenderResult } from './createRenderer';
|
|
10
|
+
|
|
11
|
+
function capitalize(string: string): string {
|
|
12
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SlotTestingOptions {
|
|
16
|
+
/**
|
|
17
|
+
* A custom React component to test if the receiving props are correct.
|
|
18
|
+
*
|
|
19
|
+
* It must:
|
|
20
|
+
* - contains at least one DOM which has `data-testid="custom"`
|
|
21
|
+
* - spread `className` to the DOM
|
|
22
|
+
*
|
|
23
|
+
* If not provided, the default custom component tests if the class name is spread.
|
|
24
|
+
*/
|
|
25
|
+
testWithComponent?: React.ComponentType;
|
|
26
|
+
/**
|
|
27
|
+
* A custom HTML tag to use for the `slots` prop.
|
|
28
|
+
*/
|
|
29
|
+
testWithElement?: keyof React.JSX.IntrinsicElements | null;
|
|
30
|
+
/**
|
|
31
|
+
* To ensure that the slot has this class name when `slotProps` is provided.
|
|
32
|
+
*/
|
|
33
|
+
expectedClassName: string;
|
|
34
|
+
isOptional?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SlotTestOverride {
|
|
38
|
+
slotName: string;
|
|
39
|
+
slotClassName?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ConformanceOptions {
|
|
43
|
+
muiName: string;
|
|
44
|
+
classes: { root: string };
|
|
45
|
+
refInstanceof: any;
|
|
46
|
+
after?: () => void;
|
|
47
|
+
inheritComponent?: React.ElementType;
|
|
48
|
+
render: (node: React.ReactElement<any>) => MuiRenderResult;
|
|
49
|
+
mount?: (node: React.ReactElement<any>) => ReactWrapper;
|
|
50
|
+
only?: Array<keyof typeof fullSuite>;
|
|
51
|
+
skip?: Array<keyof typeof fullSuite | 'classesRoot'>;
|
|
52
|
+
testComponentsRootPropWith?: string;
|
|
53
|
+
testComponentPropWith?: string | React.ElementType;
|
|
54
|
+
testDeepOverrides?: SlotTestOverride | SlotTestOverride[];
|
|
55
|
+
testRootOverrides?: SlotTestOverride;
|
|
56
|
+
testStateOverrides?: { prop?: string; value?: any; styleKey: string };
|
|
57
|
+
testCustomVariant?: boolean;
|
|
58
|
+
testVariantProps?: object;
|
|
59
|
+
testLegacyComponentsProp?: boolean;
|
|
60
|
+
wrapMount?: (
|
|
61
|
+
mount: (node: React.ReactElement<any>) => ReactWrapper,
|
|
62
|
+
) => (node: React.ReactElement<any>) => ReactWrapper;
|
|
63
|
+
slots?: Record<string, SlotTestingOptions>;
|
|
64
|
+
ThemeProvider?: React.ElementType;
|
|
65
|
+
createTheme?: (arg: any) => any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {object} node
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
72
|
+
function assertDOMNode(node: unknown) {
|
|
73
|
+
// duck typing a DOM node
|
|
74
|
+
expect(typeof (node as HTMLElement).nodeName).to.equal('string');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Utility method to make assertions about the ref on an element
|
|
79
|
+
* The element should have a component wrapped in withStyles as the root
|
|
80
|
+
*/
|
|
81
|
+
function testRef(
|
|
82
|
+
element: React.ReactElement<any>,
|
|
83
|
+
mount: ConformanceOptions['mount'],
|
|
84
|
+
onRef: (instance: unknown, wrapper: import('enzyme').ReactWrapper) => void = assertDOMNode,
|
|
85
|
+
) {
|
|
86
|
+
if (!mount) {
|
|
87
|
+
throwMissingPropError('mount');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ref = React.createRef();
|
|
91
|
+
|
|
92
|
+
const wrapper = mount(<React.Fragment>{React.cloneElement(element, { ref })}</React.Fragment>);
|
|
93
|
+
onRef(ref.current, wrapper);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Glossary
|
|
98
|
+
* - root component:
|
|
99
|
+
* - renders the outermost host component
|
|
100
|
+
* - has the `root` class if the component has one
|
|
101
|
+
* - excess props are spread to this component
|
|
102
|
+
* - has the type of `inheritComponent`
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the component with the same constructor as `component` that renders
|
|
107
|
+
* the outermost host
|
|
108
|
+
*/
|
|
109
|
+
export function findRootComponent(wrapper: ReactWrapper, component: string | React.ElementType) {
|
|
110
|
+
const outermostHostElement = findOutermostIntrinsic(wrapper).getElement();
|
|
111
|
+
|
|
112
|
+
return wrapper.find(component as string).filterWhere((componentWrapper) => {
|
|
113
|
+
return componentWrapper.contains(outermostHostElement);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function randomStringValue() {
|
|
118
|
+
return `s${Math.random().toString(36).slice(2)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function throwMissingPropError(field: string): never {
|
|
122
|
+
throw new Error(`missing "${field}" in options
|
|
123
|
+
|
|
124
|
+
> describeConformance(element, () => options)
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* MUI components have a `className` prop. The `className` is applied to
|
|
130
|
+
* the root component.
|
|
131
|
+
*/
|
|
132
|
+
export function testClassName(
|
|
133
|
+
element: React.ReactElement<any>,
|
|
134
|
+
getOptions: () => ConformanceOptions,
|
|
135
|
+
) {
|
|
136
|
+
it('applies the className to the root component', () => {
|
|
137
|
+
const { mount } = getOptions();
|
|
138
|
+
if (!mount) {
|
|
139
|
+
throwMissingPropError('mount');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const className = randomStringValue();
|
|
143
|
+
|
|
144
|
+
const wrapper = mount(React.cloneElement(element, { className }));
|
|
145
|
+
|
|
146
|
+
expect(findOutermostIntrinsic(wrapper).instance()).to.have.class(className);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* MUI components have a `component` prop that allows rendering a different
|
|
152
|
+
* Component from @inheritComponent
|
|
153
|
+
*/
|
|
154
|
+
export function testComponentProp(
|
|
155
|
+
element: React.ReactElement<any>,
|
|
156
|
+
getOptions: () => ConformanceOptions,
|
|
157
|
+
) {
|
|
158
|
+
describe('prop: component', () => {
|
|
159
|
+
it('can render another root component with the `component` prop', () => {
|
|
160
|
+
const { mount, testComponentPropWith: component = 'em' } = getOptions();
|
|
161
|
+
if (!mount) {
|
|
162
|
+
throwMissingPropError('mount');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const wrapper = mount(React.cloneElement(element, { component }));
|
|
166
|
+
|
|
167
|
+
expect(findRootComponent(wrapper, component).exists()).to.equal(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* MUI components can spread additional props to a documented component.
|
|
174
|
+
*/
|
|
175
|
+
export function testPropsSpread(
|
|
176
|
+
element: React.ReactElement<any>,
|
|
177
|
+
getOptions: () => ConformanceOptions,
|
|
178
|
+
) {
|
|
179
|
+
it(`spreads props to the root component`, () => {
|
|
180
|
+
// type def in ConformanceOptions
|
|
181
|
+
const { inheritComponent, mount } = getOptions();
|
|
182
|
+
if (!mount) {
|
|
183
|
+
throwMissingPropError('mount');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (inheritComponent === undefined) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
'Unable to test props spread without `inheritComponent`. Either skip the test or pass a React element type.',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const testProp = 'data-test-props-spread';
|
|
193
|
+
const value = randomStringValue();
|
|
194
|
+
|
|
195
|
+
const wrapper = mount(React.cloneElement(element, { [testProp]: value }));
|
|
196
|
+
const root = findRootComponent(wrapper, inheritComponent);
|
|
197
|
+
|
|
198
|
+
expect(root.props()).to.have.property(testProp, value);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Tests that the `ref` of a component will return the correct instance
|
|
204
|
+
*
|
|
205
|
+
* This is determined by a given constructor i.e. a React.Component or HTMLElement for
|
|
206
|
+
* components that forward their ref and attach it to a host component.
|
|
207
|
+
*/
|
|
208
|
+
export function describeRef(
|
|
209
|
+
element: React.ReactElement<any>,
|
|
210
|
+
getOptions: () => ConformanceOptions,
|
|
211
|
+
) {
|
|
212
|
+
describe('ref', () => {
|
|
213
|
+
it(`attaches the ref`, () => {
|
|
214
|
+
// type def in ConformanceOptions
|
|
215
|
+
const { inheritComponent, mount, refInstanceof } = getOptions();
|
|
216
|
+
|
|
217
|
+
testRef(element, mount, (instance, wrapper) => {
|
|
218
|
+
expect(instance).to.be.instanceof(refInstanceof);
|
|
219
|
+
|
|
220
|
+
if (inheritComponent !== undefined && (instance as HTMLElement).nodeType === 1) {
|
|
221
|
+
const rootHost = findOutermostIntrinsic(wrapper);
|
|
222
|
+
expect(instance).to.equal(rootHost.instance());
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Tests that the root component has the root class
|
|
231
|
+
*/
|
|
232
|
+
export function testRootClass(
|
|
233
|
+
element: React.ReactElement<any>,
|
|
234
|
+
getOptions: () => ConformanceOptions,
|
|
235
|
+
) {
|
|
236
|
+
it('applies the root class to the root component if it has this class', () => {
|
|
237
|
+
const { classes, render, skip } = getOptions();
|
|
238
|
+
if (classes.root == null) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const className = randomStringValue();
|
|
243
|
+
const classesRootClassname = randomStringValue();
|
|
244
|
+
const { container } = render(
|
|
245
|
+
React.cloneElement(element, {
|
|
246
|
+
className,
|
|
247
|
+
classes: { ...classes, root: `${classes.root} ${classesRootClassname}` },
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// we established that the root component renders the outermost host previously. We immediately
|
|
252
|
+
// jump to the host component because some components pass the `root` class
|
|
253
|
+
// to the `classes` prop of the root component.
|
|
254
|
+
// https://github.com/mui/material-ui/blob/f9896bcd129a1209153106296b3d2487547ba205/packages/material-ui/src/OutlinedInput/OutlinedInput.js#L101
|
|
255
|
+
expect(container.firstChild).to.have.class(className);
|
|
256
|
+
expect(container.firstChild).to.have.class(classes.root);
|
|
257
|
+
expect(document.querySelectorAll(`.${classes.root}`).length).to.equal(1);
|
|
258
|
+
|
|
259
|
+
// classes test only for @mui/material
|
|
260
|
+
if (!skip || !skip.includes('classesRoot')) {
|
|
261
|
+
// Test that classes prop works
|
|
262
|
+
expect(container.firstChild).to.have.class(classesRootClassname);
|
|
263
|
+
|
|
264
|
+
// Test that `classes` does not spread to DOM
|
|
265
|
+
expect(document.querySelectorAll('[classes]').length).to.equal(0);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Tests that the component can be rendered with react-test-renderer.
|
|
272
|
+
* This is important for snapshot testing with Jest (even if we don't encourage it).
|
|
273
|
+
*/
|
|
274
|
+
export function testReactTestRenderer(element: React.ReactElement<any>) {
|
|
275
|
+
it('should render without errors in ReactTestRenderer', () => {
|
|
276
|
+
ReactTestRenderer.act(() => {
|
|
277
|
+
ReactTestRenderer.create(element, {
|
|
278
|
+
createNodeMock: (node) => {
|
|
279
|
+
return document.createElement(node.type as keyof HTMLElementTagNameMap);
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function forEachSlot(
|
|
287
|
+
slots: ConformanceOptions['slots'],
|
|
288
|
+
callback: (slotName: string, slot: SlotTestingOptions) => void,
|
|
289
|
+
) {
|
|
290
|
+
if (!slots) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const slotNames = Object.keys(slots);
|
|
295
|
+
slotNames.forEach((slotName) => {
|
|
296
|
+
const slot = slots[slotName];
|
|
297
|
+
callback(slotName, slot);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function testSlotsProp(element: React.ReactElement<any>, getOptions: () => ConformanceOptions) {
|
|
302
|
+
const { render, slots, testLegacyComponentsProp } = getOptions();
|
|
303
|
+
|
|
304
|
+
const CustomComponent = React.forwardRef<
|
|
305
|
+
HTMLElement,
|
|
306
|
+
React.PropsWithChildren<{ className?: string }>
|
|
307
|
+
>(({ className, children }, ref) => (
|
|
308
|
+
<i className={className} ref={ref} data-testid="custom">
|
|
309
|
+
{children}
|
|
310
|
+
</i>
|
|
311
|
+
));
|
|
312
|
+
|
|
313
|
+
forEachSlot(slots, (slotName, slotOptions) => {
|
|
314
|
+
it(`allows overriding the ${slotName} slot with a component using the slots.${slotName} prop`, () => {
|
|
315
|
+
if (!render) {
|
|
316
|
+
throwMissingPropError('render');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const slotComponent = slotOptions.testWithComponent ?? CustomComponent;
|
|
320
|
+
|
|
321
|
+
const components = {
|
|
322
|
+
[slotName]: slotComponent,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const { queryByTestId } = render(React.cloneElement(element, { slots: components }));
|
|
326
|
+
const renderedElement = queryByTestId('custom');
|
|
327
|
+
expect(renderedElement).not.to.equal(null);
|
|
328
|
+
if (slotOptions.expectedClassName) {
|
|
329
|
+
expect(renderedElement).to.have.class(slotOptions.expectedClassName);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (slotOptions.testWithElement !== null) {
|
|
334
|
+
it(`allows overriding the ${slotName} slot with an element using the slots.${slotName} prop`, () => {
|
|
335
|
+
if (!render) {
|
|
336
|
+
throwMissingPropError('render');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const slotElement = slotOptions.testWithElement ?? 'i';
|
|
340
|
+
|
|
341
|
+
const components = {
|
|
342
|
+
[slotName]: slotElement,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const slotProps = {
|
|
346
|
+
[slotName]: {
|
|
347
|
+
'data-testid': 'customized',
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const { queryByTestId } = render(
|
|
352
|
+
React.cloneElement(element, { slots: components, slotProps }),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const renderedElement = queryByTestId('customized');
|
|
356
|
+
expect(renderedElement).not.to.equal(null);
|
|
357
|
+
|
|
358
|
+
expect(renderedElement!.nodeName.toLowerCase()).to.equal(slotElement);
|
|
359
|
+
if (slotOptions.expectedClassName) {
|
|
360
|
+
expect(renderedElement).to.have.class(slotOptions.expectedClassName);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// For testing Material UI components v5, and v6. Likely to be removed in v7.
|
|
366
|
+
if (testLegacyComponentsProp) {
|
|
367
|
+
it(`allows overriding the ${slotName} slot with a component using the components.${capitalize(
|
|
368
|
+
slotName,
|
|
369
|
+
)} prop`, () => {
|
|
370
|
+
if (!render) {
|
|
371
|
+
throwMissingPropError('render');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const slotComponent = slotOptions.testWithComponent ?? CustomComponent;
|
|
375
|
+
|
|
376
|
+
const components = {
|
|
377
|
+
[capitalize(slotName)]: slotComponent,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const { queryByTestId } = render(React.cloneElement(element, { components }));
|
|
381
|
+
const renderedElement = queryByTestId('custom');
|
|
382
|
+
expect(renderedElement).not.to.equal(null);
|
|
383
|
+
if (slotOptions.expectedClassName) {
|
|
384
|
+
expect(renderedElement).to.have.class(slotOptions.expectedClassName);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it(`prioritizes the 'slots.${slotName}' over components.${capitalize(
|
|
389
|
+
slotName,
|
|
390
|
+
)} if both are defined`, () => {
|
|
391
|
+
if (!render) {
|
|
392
|
+
throwMissingPropError('render');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const ComponentForComponentsProp = React.forwardRef<
|
|
396
|
+
HTMLDivElement,
|
|
397
|
+
{ children: React.ReactNode }
|
|
398
|
+
>(({ children }, ref) => {
|
|
399
|
+
const SlotComponent = slotOptions.testWithComponent ?? 'div';
|
|
400
|
+
return (
|
|
401
|
+
<SlotComponent ref={ref} data-testid="from-components">
|
|
402
|
+
{children}
|
|
403
|
+
</SlotComponent>
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const ComponentForSlotsProp = React.forwardRef<
|
|
408
|
+
HTMLDivElement,
|
|
409
|
+
{ children: React.ReactNode }
|
|
410
|
+
>(({ children }, ref) => {
|
|
411
|
+
const SlotComponent = slotOptions.testWithComponent ?? 'div';
|
|
412
|
+
return (
|
|
413
|
+
<SlotComponent ref={ref} data-testid="from-slots">
|
|
414
|
+
{children}
|
|
415
|
+
</SlotComponent>
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const components = {
|
|
420
|
+
[capitalize(slotName)]: ComponentForComponentsProp,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const slotOverrides = {
|
|
424
|
+
[slotName]: ComponentForSlotsProp,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const { queryByTestId } = render(
|
|
428
|
+
React.cloneElement(element, { components, slots: slotOverrides }),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(queryByTestId('from-slots')).not.to.equal(null);
|
|
432
|
+
expect(queryByTestId('from-components')).to.equal(null);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (slotOptions.testWithElement !== null) {
|
|
436
|
+
it(`allows overriding the ${slotName} slot with an element using the components.${capitalize(
|
|
437
|
+
slotName,
|
|
438
|
+
)} prop`, () => {
|
|
439
|
+
if (!render) {
|
|
440
|
+
throwMissingPropError('render');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const slotElement = slotOptions.testWithElement ?? 'i';
|
|
444
|
+
|
|
445
|
+
const components = {
|
|
446
|
+
[capitalize(slotName)]: slotElement,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const componentsProps = {
|
|
450
|
+
[slotName]: {
|
|
451
|
+
'data-testid': 'customized',
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const { queryByTestId } = render(
|
|
456
|
+
React.cloneElement(element, { components, componentsProps }),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const renderedElement = queryByTestId('customized');
|
|
460
|
+
expect(renderedElement).not.to.equal(null);
|
|
461
|
+
|
|
462
|
+
expect(renderedElement!.nodeName.toLowerCase()).to.equal(slotElement);
|
|
463
|
+
if (slotOptions.expectedClassName) {
|
|
464
|
+
expect(renderedElement).to.have.class(slotOptions.expectedClassName);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function testSlotPropsProp(element: React.ReactElement<any>, getOptions: () => ConformanceOptions) {
|
|
473
|
+
const { render, slots, testLegacyComponentsProp } = getOptions();
|
|
474
|
+
|
|
475
|
+
if (!render) {
|
|
476
|
+
throwMissingPropError('render');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
forEachSlot(slots, (slotName, slotOptions) => {
|
|
480
|
+
it(`sets custom properties on the ${slotName} slot's element with the slotProps.${slotName} prop`, () => {
|
|
481
|
+
const slotProps = {
|
|
482
|
+
[slotName]: {
|
|
483
|
+
'data-testid': 'custom',
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const { queryByTestId } = render(React.cloneElement(element, { slotProps }));
|
|
488
|
+
const slotComponent = queryByTestId('custom');
|
|
489
|
+
expect(slotComponent).not.to.equal(null);
|
|
490
|
+
|
|
491
|
+
if (slotOptions.expectedClassName) {
|
|
492
|
+
expect(slotComponent).to.have.class(slotOptions.expectedClassName);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
if (slotOptions.expectedClassName) {
|
|
497
|
+
it(`merges the class names provided in slotsProps.${slotName} with the built-in ones`, () => {
|
|
498
|
+
const slotProps = {
|
|
499
|
+
[slotName]: {
|
|
500
|
+
'data-testid': 'custom',
|
|
501
|
+
className: randomStringValue(),
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const { getByTestId } = render(React.cloneElement(element, { slotProps }));
|
|
506
|
+
|
|
507
|
+
expect(getByTestId('custom')).to.have.class(slotOptions.expectedClassName);
|
|
508
|
+
expect(getByTestId('custom')).to.have.class(slotProps[slotName].className);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (testLegacyComponentsProp) {
|
|
513
|
+
it(`sets custom properties on the ${slotName} slot's element with the componentsProps.${slotName} prop`, () => {
|
|
514
|
+
const componentsProps = {
|
|
515
|
+
[slotName]: {
|
|
516
|
+
'data-testid': 'custom',
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const { queryByTestId } = render(React.cloneElement(element, { componentsProps }));
|
|
521
|
+
const slotComponent = queryByTestId('custom');
|
|
522
|
+
expect(slotComponent).not.to.equal(null);
|
|
523
|
+
|
|
524
|
+
if (slotOptions.expectedClassName) {
|
|
525
|
+
expect(slotComponent).to.have.class(slotOptions.expectedClassName);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it(`prioritizes the 'slotProps.${slotName}' over componentsProps.${slotName} if both are defined`, () => {
|
|
530
|
+
const componentsProps = {
|
|
531
|
+
[slotName]: {
|
|
532
|
+
'data-testid': 'custom',
|
|
533
|
+
'data-from-components-props': 'true',
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const slotProps = {
|
|
538
|
+
[slotName]: {
|
|
539
|
+
'data-testid': 'custom',
|
|
540
|
+
'data-from-slot-props': 'true',
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const { queryByTestId } = render(
|
|
545
|
+
React.cloneElement(element, { componentsProps, slotProps }),
|
|
546
|
+
);
|
|
547
|
+
const slotComponent = queryByTestId('custom');
|
|
548
|
+
expect(slotComponent).to.have.attribute('data-from-slot-props', 'true');
|
|
549
|
+
expect(slotComponent).not.to.have.attribute('data-from-components-props');
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function testSlotPropsCallback(
|
|
556
|
+
element: React.ReactElement<any>,
|
|
557
|
+
getOptions: () => ConformanceOptions,
|
|
558
|
+
) {
|
|
559
|
+
const { render, slots } = getOptions();
|
|
560
|
+
|
|
561
|
+
if (!render) {
|
|
562
|
+
throwMissingPropError('render');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
forEachSlot(slots, (slotName) => {
|
|
566
|
+
it(`sets custom properties on the ${slotName} slot's element with the slotProps.${slotName} callback`, () => {
|
|
567
|
+
const slotProps = {
|
|
568
|
+
[slotName]: (ownerState: Record<string, any>) => ({
|
|
569
|
+
'data-testid': ownerState.className,
|
|
570
|
+
}),
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const { queryByTestId } = render(
|
|
574
|
+
React.cloneElement(element, { slotProps, className: 'custom' }),
|
|
575
|
+
);
|
|
576
|
+
const slotComponent = queryByTestId('custom');
|
|
577
|
+
expect(slotComponent).not.to.equal(null);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* MUI components have a `components` prop that allows rendering a different
|
|
584
|
+
* Components from @inheritComponent
|
|
585
|
+
*/
|
|
586
|
+
function testComponentsProp(
|
|
587
|
+
element: React.ReactElement<any>,
|
|
588
|
+
getOptions: () => ConformanceOptions,
|
|
589
|
+
) {
|
|
590
|
+
describe('prop components:', () => {
|
|
591
|
+
it('can render another root component with the `components` prop', () => {
|
|
592
|
+
const { mount, testComponentsRootPropWith: component = 'em' } = getOptions();
|
|
593
|
+
if (!mount) {
|
|
594
|
+
throwMissingPropError('mount');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const wrapper = mount(React.cloneElement(element, { components: { Root: component } }));
|
|
598
|
+
|
|
599
|
+
expect(findRootComponent(wrapper, component).exists()).to.equal(true);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* MUI theme has a components section that allows specifying default props.
|
|
606
|
+
* Components from @inheritComponent
|
|
607
|
+
*/
|
|
608
|
+
function testThemeDefaultProps(
|
|
609
|
+
element: React.ReactElement<any>,
|
|
610
|
+
getOptions: () => ConformanceOptions,
|
|
611
|
+
) {
|
|
612
|
+
describe('theme default components:', () => {
|
|
613
|
+
it("respect theme's defaultProps", () => {
|
|
614
|
+
const testProp = 'data-id';
|
|
615
|
+
const { muiName, render, ThemeProvider, createTheme } = getOptions();
|
|
616
|
+
|
|
617
|
+
if (!muiName) {
|
|
618
|
+
throwMissingPropError('muiName');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!render) {
|
|
622
|
+
throwMissingPropError('render');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!ThemeProvider) {
|
|
626
|
+
throwMissingPropError('ThemeProvider');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!createTheme) {
|
|
630
|
+
throwMissingPropError('createTheme');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const theme = createTheme({
|
|
634
|
+
components: {
|
|
635
|
+
[muiName]: {
|
|
636
|
+
defaultProps: {
|
|
637
|
+
[testProp]: 'testProp',
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const { container } = render(<ThemeProvider theme={theme}>{element}</ThemeProvider>);
|
|
644
|
+
|
|
645
|
+
expect(container.firstChild).to.have.attribute(testProp, 'testProp');
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* MUI theme has a components section that allows specifying style overrides.
|
|
652
|
+
* Components from @inheritComponent
|
|
653
|
+
*/
|
|
654
|
+
function testThemeStyleOverrides(
|
|
655
|
+
element: React.ReactElement<any>,
|
|
656
|
+
getOptions: () => ConformanceOptions,
|
|
657
|
+
) {
|
|
658
|
+
describe('theme style overrides:', () => {
|
|
659
|
+
it("respect theme's styleOverrides custom state", function test() {
|
|
660
|
+
if (/jsdom/.test(window.navigator.userAgent)) {
|
|
661
|
+
this.skip();
|
|
662
|
+
}
|
|
663
|
+
const { muiName, testStateOverrides, render, ThemeProvider, createTheme } = getOptions();
|
|
664
|
+
|
|
665
|
+
if (!testStateOverrides) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!muiName) {
|
|
670
|
+
throwMissingPropError('muiName');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!render) {
|
|
674
|
+
throwMissingPropError('render');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (!ThemeProvider) {
|
|
678
|
+
throwMissingPropError('ThemeProvider');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!createTheme) {
|
|
682
|
+
throwMissingPropError('createTheme');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const testStyle = {
|
|
686
|
+
marginTop: '13px',
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const theme = createTheme({
|
|
690
|
+
components: {
|
|
691
|
+
[muiName]: {
|
|
692
|
+
styleOverrides: {
|
|
693
|
+
[testStateOverrides.styleKey]: testStyle,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (!testStateOverrides.prop) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const { container } = render(
|
|
704
|
+
<ThemeProvider theme={theme}>
|
|
705
|
+
{React.cloneElement(element, {
|
|
706
|
+
[testStateOverrides.prop]: testStateOverrides.value,
|
|
707
|
+
})}
|
|
708
|
+
</ThemeProvider>,
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
expect(container.firstChild).to.toHaveComputedStyle(testStyle);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("respect theme's styleOverrides slots", function test() {
|
|
715
|
+
if (/jsdom/.test(window.navigator.userAgent)) {
|
|
716
|
+
this.skip();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const {
|
|
720
|
+
muiName,
|
|
721
|
+
testDeepOverrides,
|
|
722
|
+
testRootOverrides = { slotName: 'root' },
|
|
723
|
+
render,
|
|
724
|
+
ThemeProvider,
|
|
725
|
+
createTheme,
|
|
726
|
+
} = getOptions();
|
|
727
|
+
|
|
728
|
+
if (!ThemeProvider) {
|
|
729
|
+
throwMissingPropError('ThemeProvider');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!createTheme) {
|
|
733
|
+
throwMissingPropError('createTheme');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const testStyle = {
|
|
737
|
+
mixBlendMode: 'darken',
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
function resolveDeepOverrides(
|
|
741
|
+
callback: (styles: Record<string, any>, slot: SlotTestOverride) => void,
|
|
742
|
+
) {
|
|
743
|
+
if (!testDeepOverrides) {
|
|
744
|
+
return {};
|
|
745
|
+
}
|
|
746
|
+
const styles = {};
|
|
747
|
+
if (Array.isArray(testDeepOverrides)) {
|
|
748
|
+
testDeepOverrides.forEach((slot) => {
|
|
749
|
+
callback(styles, slot);
|
|
750
|
+
});
|
|
751
|
+
} else {
|
|
752
|
+
callback(styles, testDeepOverrides);
|
|
753
|
+
}
|
|
754
|
+
return styles;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const theme = createTheme({
|
|
758
|
+
components: {
|
|
759
|
+
[muiName]: {
|
|
760
|
+
styleOverrides: {
|
|
761
|
+
[testRootOverrides.slotName]: {
|
|
762
|
+
...testStyle,
|
|
763
|
+
...resolveDeepOverrides((styles, slot) => {
|
|
764
|
+
styles[`& .${slot.slotClassName}`] = {
|
|
765
|
+
fontVariantCaps: 'all-petite-caps',
|
|
766
|
+
};
|
|
767
|
+
}),
|
|
768
|
+
},
|
|
769
|
+
...resolveDeepOverrides((styles, slot) => {
|
|
770
|
+
styles[slot.slotName] = {
|
|
771
|
+
mixBlendMode: 'darken',
|
|
772
|
+
};
|
|
773
|
+
}),
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const { container, setProps } = render(
|
|
780
|
+
<ThemeProvider theme={theme}>{element}</ThemeProvider>,
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
if (testRootOverrides.slotClassName) {
|
|
784
|
+
expect(
|
|
785
|
+
document.querySelector(`.${testRootOverrides.slotClassName}`),
|
|
786
|
+
).to.toHaveComputedStyle(testStyle);
|
|
787
|
+
} else {
|
|
788
|
+
expect(container.firstChild).to.toHaveComputedStyle(testStyle);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (testDeepOverrides) {
|
|
792
|
+
(Array.isArray(testDeepOverrides) ? testDeepOverrides : [testDeepOverrides]).forEach(
|
|
793
|
+
(slot) => {
|
|
794
|
+
expect(document.querySelector(`.${slot.slotClassName}`)).to.toHaveComputedStyle({
|
|
795
|
+
fontVariantCaps: 'all-petite-caps',
|
|
796
|
+
mixBlendMode: 'darken',
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
const themeWithoutRootOverrides = createTheme({
|
|
802
|
+
components: {
|
|
803
|
+
[muiName]: {
|
|
804
|
+
styleOverrides: {
|
|
805
|
+
...resolveDeepOverrides((styles, slot) => {
|
|
806
|
+
styles[slot.slotName] = testStyle;
|
|
807
|
+
}),
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
setProps({ theme: themeWithoutRootOverrides });
|
|
814
|
+
|
|
815
|
+
(Array.isArray(testDeepOverrides) ? testDeepOverrides : [testDeepOverrides]).forEach(
|
|
816
|
+
(slot) => {
|
|
817
|
+
expect(document.querySelector(`.${slot.slotClassName}`)).to.toHaveComputedStyle(
|
|
818
|
+
testStyle,
|
|
819
|
+
);
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('overrideStyles does not replace each other in slots', function test() {
|
|
826
|
+
if (/jsdom/.test(window.navigator.userAgent)) {
|
|
827
|
+
this.skip();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const { muiName, classes, testStateOverrides, render, ThemeProvider, createTheme } =
|
|
831
|
+
getOptions();
|
|
832
|
+
|
|
833
|
+
if (!ThemeProvider) {
|
|
834
|
+
throwMissingPropError('ThemeProvider');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (!createTheme) {
|
|
838
|
+
throwMissingPropError('createTheme');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const classKeys = Object.keys(classes);
|
|
842
|
+
|
|
843
|
+
// only test the component that has `root` and other classKey
|
|
844
|
+
if (!testStateOverrides || !classKeys.includes('root') || classKeys.length === 1) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// `styleKey` in some tests is `foo` or `bar`, so need to check if it is a valid classKey.
|
|
849
|
+
const isStyleKeyExists = classKeys.indexOf(testStateOverrides.styleKey) !== -1;
|
|
850
|
+
|
|
851
|
+
if (!isStyleKeyExists) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const theme = createTheme({
|
|
856
|
+
components: {
|
|
857
|
+
[muiName]: {
|
|
858
|
+
styleOverrides: {
|
|
859
|
+
root: {
|
|
860
|
+
[`&.${classes.root}`]: {
|
|
861
|
+
filter: 'blur(1px)',
|
|
862
|
+
mixBlendMode: 'darken',
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
...(testStateOverrides && {
|
|
866
|
+
[testStateOverrides.styleKey]: {
|
|
867
|
+
[`&.${classes.root}`]: {
|
|
868
|
+
mixBlendMode: 'color',
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
}),
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
if (!testStateOverrides.prop) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
render(
|
|
882
|
+
<ThemeProvider theme={theme}>
|
|
883
|
+
{React.cloneElement(element, {
|
|
884
|
+
[testStateOverrides.prop]: testStateOverrides.value,
|
|
885
|
+
})}
|
|
886
|
+
</ThemeProvider>,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
expect(document.querySelector(`.${classes.root}`)).toHaveComputedStyle({
|
|
890
|
+
filter: 'blur(1px)', // still valid in root
|
|
891
|
+
mixBlendMode: 'color', // overridden by `styleKey`
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* MUI theme has a components section that allows specifying custom variants.
|
|
899
|
+
* Components from @inheritComponent
|
|
900
|
+
*/
|
|
901
|
+
function testThemeVariants(element: React.ReactElement<any>, getOptions: () => ConformanceOptions) {
|
|
902
|
+
describe('theme variants:', () => {
|
|
903
|
+
it("respect theme's variants", function test() {
|
|
904
|
+
if (/jsdom/.test(window.navigator.userAgent)) {
|
|
905
|
+
this.skip();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const { muiName, testVariantProps, render, ThemeProvider, createTheme } = getOptions();
|
|
909
|
+
|
|
910
|
+
if (!testVariantProps) {
|
|
911
|
+
throw new Error('missing testVariantProps');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!muiName) {
|
|
915
|
+
throwMissingPropError('muiName');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (!render) {
|
|
919
|
+
throwMissingPropError('render');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!ThemeProvider) {
|
|
923
|
+
throwMissingPropError('ThemeProvider');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (!createTheme) {
|
|
927
|
+
throwMissingPropError('createTheme');
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const testStyle = {
|
|
931
|
+
mixBlendMode: 'darken',
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const theme = createTheme({
|
|
935
|
+
components: {
|
|
936
|
+
[muiName]: {
|
|
937
|
+
variants: [
|
|
938
|
+
{
|
|
939
|
+
props: testVariantProps,
|
|
940
|
+
style: testStyle,
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const { getByTestId } = render(
|
|
948
|
+
<ThemeProvider theme={theme}>
|
|
949
|
+
{React.cloneElement(element, { ...testVariantProps, 'data-testid': 'with-props' })}
|
|
950
|
+
{React.cloneElement(element, { 'data-testid': 'without-props' })}
|
|
951
|
+
</ThemeProvider>,
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
expect(getByTestId('with-props')).to.toHaveComputedStyle(testStyle);
|
|
955
|
+
expect(getByTestId('without-props')).not.to.toHaveComputedStyle(testStyle);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('supports custom variant', function test() {
|
|
959
|
+
if (/jsdom/.test(window.navigator.userAgent)) {
|
|
960
|
+
this.skip();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const { muiName, testCustomVariant, render, ThemeProvider, createTheme } = getOptions();
|
|
964
|
+
|
|
965
|
+
if (!ThemeProvider) {
|
|
966
|
+
throwMissingPropError('ThemeProvider');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (!createTheme) {
|
|
970
|
+
throwMissingPropError('createTheme');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (!testCustomVariant) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const theme = createTheme({
|
|
978
|
+
components: {
|
|
979
|
+
[muiName]: {
|
|
980
|
+
styleOverrides: {
|
|
981
|
+
root: ({ ownerState }: { ownerState: any }) => ({
|
|
982
|
+
...(ownerState.variant === 'unknown' && {
|
|
983
|
+
mixBlendMode: 'darken',
|
|
984
|
+
}),
|
|
985
|
+
}),
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const { getByTestId } = render(
|
|
992
|
+
<ThemeProvider theme={theme}>
|
|
993
|
+
{React.cloneElement(element, { variant: 'unknown', 'data-testid': 'custom-variant' })}
|
|
994
|
+
</ThemeProvider>,
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
expect(getByTestId('custom-variant')).toHaveComputedStyle({ mixBlendMode: 'darken' });
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* MUI theme supports custom palettes.
|
|
1004
|
+
* The components that iterate over the palette via `variants` should be able to render with or without applying the custom palette styles.
|
|
1005
|
+
*/
|
|
1006
|
+
function testThemeCustomPalette(
|
|
1007
|
+
element: React.ReactElement<any>,
|
|
1008
|
+
getOptions: () => ConformanceOptions,
|
|
1009
|
+
) {
|
|
1010
|
+
describe('theme extended palette:', () => {
|
|
1011
|
+
it('should render without errors', function test() {
|
|
1012
|
+
const { render, ThemeProvider, createTheme } = getOptions();
|
|
1013
|
+
if (!/jsdom/.test(window.navigator.userAgent) || !render || !ThemeProvider || !createTheme) {
|
|
1014
|
+
this.skip();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const theme = createTheme({
|
|
1018
|
+
palette: {
|
|
1019
|
+
custom: {
|
|
1020
|
+
main: '#ff5252',
|
|
1021
|
+
},
|
|
1022
|
+
unknown: null,
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
expect(() => render(<ThemeProvider theme={theme}>{element}</ThemeProvider>)).not.to.throw();
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const fullSuite = {
|
|
1032
|
+
componentProp: testComponentProp,
|
|
1033
|
+
componentsProp: testComponentsProp,
|
|
1034
|
+
mergeClassName: testClassName,
|
|
1035
|
+
propsSpread: testPropsSpread,
|
|
1036
|
+
refForwarding: describeRef,
|
|
1037
|
+
rootClass: testRootClass,
|
|
1038
|
+
reactTestRenderer: testReactTestRenderer,
|
|
1039
|
+
slotPropsProp: testSlotPropsProp,
|
|
1040
|
+
slotPropsCallback: testSlotPropsCallback,
|
|
1041
|
+
slotsProp: testSlotsProp,
|
|
1042
|
+
themeDefaultProps: testThemeDefaultProps,
|
|
1043
|
+
themeStyleOverrides: testThemeStyleOverrides,
|
|
1044
|
+
themeVariants: testThemeVariants,
|
|
1045
|
+
themeCustomPalette: testThemeCustomPalette,
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Tests various aspects of a component that should be equal across MUI
|
|
1050
|
+
* components.
|
|
1051
|
+
*/
|
|
1052
|
+
function describeConformance(
|
|
1053
|
+
minimalElement: React.ReactElement<any>,
|
|
1054
|
+
getOptions: () => ConformanceOptions,
|
|
1055
|
+
) {
|
|
1056
|
+
let originalMatchmedia: typeof window.matchMedia;
|
|
1057
|
+
const storage: Record<string, string> = {};
|
|
1058
|
+
beforeEach(() => {
|
|
1059
|
+
originalMatchmedia = window.matchMedia;
|
|
1060
|
+
// Create mocks of localStorage getItem and setItem functions
|
|
1061
|
+
Object.defineProperty(global, 'localStorage', {
|
|
1062
|
+
value: {
|
|
1063
|
+
getItem: (key: string) => storage[key],
|
|
1064
|
+
setItem: (key: string, value: string) => {
|
|
1065
|
+
storage[key] = value;
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
configurable: true,
|
|
1069
|
+
});
|
|
1070
|
+
window.matchMedia = () =>
|
|
1071
|
+
({
|
|
1072
|
+
addListener: () => {},
|
|
1073
|
+
removeListener: () => {},
|
|
1074
|
+
}) as unknown as MediaQueryList;
|
|
1075
|
+
});
|
|
1076
|
+
afterEach(() => {
|
|
1077
|
+
window.matchMedia = originalMatchmedia;
|
|
1078
|
+
});
|
|
1079
|
+
const {
|
|
1080
|
+
after: runAfterHook = () => {},
|
|
1081
|
+
only = Object.keys(fullSuite),
|
|
1082
|
+
slots,
|
|
1083
|
+
skip = [],
|
|
1084
|
+
wrapMount,
|
|
1085
|
+
} = getOptions();
|
|
1086
|
+
|
|
1087
|
+
let filteredTests = Object.keys(fullSuite).filter(
|
|
1088
|
+
(testKey) =>
|
|
1089
|
+
only.indexOf(testKey) !== -1 && skip.indexOf(testKey as keyof typeof fullSuite) === -1,
|
|
1090
|
+
) as (keyof typeof fullSuite)[];
|
|
1091
|
+
|
|
1092
|
+
const slotBasedTests = ['slotsProp', 'slotPropsProp', 'slotPropsCallback'];
|
|
1093
|
+
|
|
1094
|
+
if (!slots) {
|
|
1095
|
+
// if `slots` are not defined, do not run tests that depend on them
|
|
1096
|
+
filteredTests = filteredTests.filter((testKey) => !slotBasedTests.includes(testKey));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const baseMount = createMount();
|
|
1100
|
+
const mount = wrapMount !== undefined ? wrapMount(baseMount) : baseMount;
|
|
1101
|
+
|
|
1102
|
+
after(runAfterHook);
|
|
1103
|
+
|
|
1104
|
+
function getTestOptions(): ConformanceOptions {
|
|
1105
|
+
return {
|
|
1106
|
+
...getOptions(),
|
|
1107
|
+
mount,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
filteredTests.forEach((testKey) => {
|
|
1112
|
+
const test = fullSuite[testKey];
|
|
1113
|
+
test(minimalElement, getTestOptions);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export default createDescribe('MUI component API', describeConformance);
|