UNPKG

7.32 kBJavaScriptView Raw
1import { expect } from 'chai';
2import * as React from 'react';
3import findOutermostIntrinsic from './findOutermostIntrinsic';
4import ReactTestRenderer from 'react-test-renderer';
5import testRef from './testRef';
6/**
7 * Glossary
8 * - root component:
9 * - renders the outermost host component
10 * - has the `root` class if the component has one
11 * - excess props are spread to this component
12 * - has the type of `inheritComponent`
13 */
14
15/**
16 * Returns the component with the same constructor as `component` that renders
17 * the outermost host
18 *
19 * @param {import('enzyme').ReactWrapper} wrapper
20 * @param {object} options
21 * @param {import('react').ElementType} component
22 */
23
24function findRootComponent(wrapper, {
25 component
26}) {
27 const outermostHostElement = findOutermostIntrinsic(wrapper).getElement();
28 return wrapper.find(component).filterWhere(componentWrapper => {
29 return componentWrapper.contains(outermostHostElement);
30 });
31}
32
33function randomStringValue() {
34 return Math.random().toString(36).slice(2);
35}
36/**
37 * Material-UI components have a `className` prop. The `className` is applied to
38 * the root component.
39 *
40 * @param {React.ReactElement} element
41 * @param {() => ConformanceOptions} getOptions
42 */
43
44
45function testClassName(element, getOptions) {
46 it('applies the className to the root component', () => {
47 const {
48 mount
49 } = getOptions();
50 const className = randomStringValue();
51 const wrapper = mount( /*#__PURE__*/React.cloneElement(element, {
52 className
53 }));
54 expect(findOutermostIntrinsic(wrapper).hasClass(className)).to.equal(true, 'does have a custom `className`');
55 });
56}
57/**
58 * Material-UI components have a `component` prop that allows rendering a different
59 * Component from @inheritComponent
60 *
61 * @param {React.ReactElement} element
62 * @param {() => ConformanceOptions} getOptions
63 */
64
65
66function testComponentProp(element, getOptions) {
67 describe('prop: component', () => {
68 it('can render another root component with the `component` prop', () => {
69 const {
70 classes,
71 mount,
72 testComponentPropWith: component = 'em'
73 } = getOptions();
74 const wrapper = mount( /*#__PURE__*/React.cloneElement(element, {
75 component
76 }));
77 expect(findRootComponent(wrapper, {
78 classes,
79 component
80 }).exists()).to.equal(true);
81 });
82 });
83}
84/**
85 * Material-UI components can spread additional props to a documented component.
86 * It's set via @inheritComponent in the source.
87 *
88 * @param {React.ReactElement} element
89 * @param {() => ConformanceOptions} getOptions
90 */
91
92
93function testPropsSpread(element, getOptions) {
94 it(`spreads props to the root component`, () => {
95 // type def in ConformanceOptions
96 const {
97 classes,
98 inheritComponent,
99 mount
100 } = getOptions();
101 const testProp = 'data-test-props-spread';
102 const value = randomStringValue();
103 const wrapper = mount( /*#__PURE__*/React.cloneElement(element, {
104 [testProp]: value
105 }));
106 const root = findRootComponent(wrapper, {
107 classes,
108 component: inheritComponent
109 });
110 expect(root.props()[testProp]).to.equal(value);
111 });
112}
113/**
114 * Tests that the `ref` of a component will return the correct instance
115 *
116 * This is determined by a given constructor i.e. a React.Component or HTMLElement for
117 * components that forward their ref and attach it to a host component.
118 *
119 * @param {React.ReactElement} element
120 * @param {() => ConformanceOptions} getOptions
121 */
122
123
124function describeRef(element, getOptions) {
125 describe('ref', () => {
126 it(`attaches the ref`, () => {
127 // type def in ConformanceOptions
128 const {
129 inheritComponent,
130 mount,
131 refInstanceof
132 } = getOptions();
133 testRef(element, mount, (instance, wrapper) => {
134 expect(instance).to.be.instanceof(refInstanceof);
135
136 if (inheritComponent && instance.nodeType === 1) {
137 const rootHost = findOutermostIntrinsic(wrapper);
138 expect(instance).to.equal(rootHost.instance());
139 }
140 });
141 });
142 });
143}
144/**
145 * Tests that the root component has the root class
146 *
147 * @param {React.ReactElement} element
148 * @param {() => ConformanceOptions} getOptions
149 */
150
151
152function testRootClass(element, getOptions) {
153 it('applies the root class to the root component if it has this class', () => {
154 const {
155 classes,
156 mount
157 } = getOptions();
158
159 if (classes.root == null) {
160 return;
161 }
162
163 const className = randomStringValue();
164 const wrapper = mount( /*#__PURE__*/React.cloneElement(element, {
165 className
166 })); // we established that the root component renders the outermost host previously. We immediately
167 // jump to the host component because some components pass the `root` class
168 // to the `classes` prop of the root component.
169 // https://github.com/mui-org/material-ui/blob/f9896bcd129a1209153106296b3d2487547ba205/packages/material-ui/src/OutlinedInput/OutlinedInput.js#L101
170
171 expect(findOutermostIntrinsic(wrapper).hasClass(classes.root)).to.equal(true);
172 expect(findOutermostIntrinsic(wrapper).hasClass(className)).to.equal(true);
173 });
174}
175/**
176 * Tests that the component can be rendered with react-test-renderer.
177 * This is important for snapshot testing with Jest (even if we don't encourage it).
178 *
179 * @param {React.ReactElement} element
180 */
181
182
183function testReactTestRenderer(element) {
184 it('should render without errors in ReactTestRenderer', () => {
185 ReactTestRenderer.act(() => {
186 ReactTestRenderer.create(element, {
187 createNodeMock: node => {
188 return document.createElement(node.type);
189 }
190 });
191 });
192 });
193}
194
195const fullSuite = {
196 componentProp: testComponentProp,
197 mergeClassName: testClassName,
198 propsSpread: testPropsSpread,
199 refForwarding: describeRef,
200 rootClass: testRootClass,
201 reactTestRenderer: testReactTestRenderer
202};
203/**
204 * @typedef {Object} ConformanceOptions
205 * @property {Record<string, string>} classes - `classes` of the component provided by `@material-ui/styles`
206 * @property {string} inheritComponent - The element type that receives spread props.
207 * @property {function} mount - Should be a return value from createMount
208 * @property {(keyof typeof fullSuite)[]} [only] - If specified only run the tests listed
209 * @property {any} refInstanceof - `ref` will be an instanceof this constructor.
210 * @property {keyof typeof fullSuite[]} [skip] - Skip the specified tests
211 * @property {string} [testComponentPropWith] - The host component that should be rendered instead.
212 */
213
214/**
215 * Tests various aspects of a component that should be equal across Material-UI
216 * components.
217 *
218 * @param {React.ReactElement} minimalElement - the component with it's minimal required props
219 * @param {() => ConformanceOptions} getOptions
220 *
221 */
222
223export default function describeConformance(minimalElement, getOptions) {
224 const {
225 after: runAfterHook = () => {},
226 only = Object.keys(fullSuite),
227 skip = []
228 } = getOptions();
229 describe('Material-UI component API', () => {
230 after(runAfterHook);
231 Object.keys(fullSuite).filter(testKey => only.indexOf(testKey) !== -1 && skip.indexOf(testKey) === -1).forEach(testKey => {
232 const test = fullSuite[testKey];
233 test(minimalElement, getOptions);
234 });
235 });
236}
\No newline at end of file