@samitouri / QOS-React / commits / 2757a53fa5

Add fixture components (#8860)

* add fixture components * add a few more options and styles * Test fixture updates - Pull in React from window global - Add field to TestCase for fix PR links - Update some styles * Remove unused Fixture.Result comment * Remove leading # from resolvedBy link * Implement tag loading utility that caches response Don't bust the cache doing feature detection COME ON * Use 'local' without version for option

Jason Quense committed Mar 2, 2017 at 14:38 UTC 2757a53fa5546236bdfa498724a270cf8db76e89
15 files changed +701 -18
fixtures/dom/package.json
+3 -1
@@ -6,9 +6,11 @@
6 "react-scripts": "0.8.4"
7 },
8 "dependencies": {
9 + "classnames": "^2.2.5",
10 "query-string": "^4.2.3",
11 "react": "^15.4.1",
11 - "react-dom": "^15.4.1"
12 + "react-dom": "^15.4.1",
13 + "semver": "^5.3.0"
14 },
15 "scripts": {
16 "start": "react-scripts start",
fixtures/dom/src/components/Fixture.js new
+21
@@ -0,0 +1,21 @@
1 +const React = window.React;
2 +
3 +const propTypes = {
4 + children: React.PropTypes.node.isRequired,
5 +};
6 +
7 +class Fixture extends React.Component {
8 + render() {
9 + const { children } = this.props;
10 +
11 + return (
12 + <div className="test-fixture">
13 + {children}
14 + </div>
15 + );
16 + }
17 +}
18 +
19 +Fixture.propTypes = propTypes;
20 +
21 +export default Fixture
fixtures/dom/src/components/FixtureSet.js new
+28
@@ -0,0 +1,28 @@
1 +import React from 'react';
2 +
3 +const propTypes = {
4 + title: React.PropTypes.node.isRequired,
5 + description: React.PropTypes.node.isRequired,
6 +};
7 +
8 +class FixtureSet extends React.Component {
9 +
10 + render() {
11 + const { title, description, children } = this.props;
12 +
13 + return (
14 + <div>
15 + <h1>{title}</h1>
16 + {description && (
17 + <p>{description}</p>
18 + )}
19 +
20 + {children}
21 + </div>
22 + );
23 + }
24 +}
25 +
26 +FixtureSet.propTypes = propTypes;
27 +
28 +export default FixtureSet
fixtures/dom/src/components/Header.js
+5 -4
@@ -1,4 +1,5 @@
1 import { parse, stringify } from 'query-string';
2 +import getVersionTags from '../tags';
3 const React = window.React;
4
5 const Header = React.createClass({
@@ -9,13 +10,12 @@ const Header = React.createClass({
10 return { version, versions };
11 },
12 componentWillMount() {
12 - fetch('https://api.github.com/repos/facebook/react/tags', { mode: 'cors' })
13 - .then(res => res.json())
13 + getVersionTags()
14 .then(tags => {
15 let versions = tags.map(tag => tag.name.slice(1));
16 - versions = ['local', ...versions];
16 + versions = [`local`, ...versions];
17 this.setState({ versions });
18 - });
18 + })
19 },
20 handleVersionChange(event) {
21 const query = parse(window.location.search);
@@ -46,6 +46,7 @@ const Header = React.createClass({
46 <option value="/text-inputs">Text Inputs</option>
47 <option value="/selects">Selects</option>
48 <option value="/textareas">Textareas</option>
49 + <option value="/input-change-events">Input change events</option>
50 </select>
51 </label>
52 <label htmlFor="react_version">
fixtures/dom/src/components/TestCase.js new
+145
@@ -0,0 +1,145 @@
1 +import cn from 'classnames';
2 +import semver from 'semver';
3 +import React from 'react';
4 +import { parse } from 'query-string';
5 +import { semverString } from './propTypes'
6 +
7 +const propTypes = {
8 + children: React.PropTypes.node.isRequired,
9 + title: React.PropTypes.node.isRequired,
10 + resolvedIn: semverString,
11 + resolvedBy: React.PropTypes.string
12 +};
13 +
14 +class TestCase extends React.Component {
15 + constructor(props, context) {
16 + super(props, context);
17 +
18 + this.state = {
19 + complete: false,
20 + };
21 + }
22 +
23 + handleChange = (e) => {
24 + this.setState({
25 + complete: e.target.checked
26 + })
27 + };
28 +
29 + render() {
30 + const {
31 + title,
32 + description,
33 + resolvedIn,
34 + resolvedBy,
35 + affectedBrowsers,
36 + children,
37 + } = this.props;
38 +
39 + let { complete } = this.state;
40 +
41 + const { version } = parse(window.location.search);
42 + const isTestRelevant = (
43 + !version ||
44 + !resolvedIn ||
45 + semver.gte(version, resolvedIn)
46 + );
47 +
48 + complete = !isTestRelevant || complete;
49 +
50 + return (
51 + <section
52 + className={cn(
53 + "test-case",
54 + complete && 'test-case--complete'
55 + )}
56 + >
57 + <h2 className="test-case__title type-subheading">
58 + <label>
59 + <input
60 + className="test-case__title__check"
61 + type="checkbox"
62 + checked={complete}
63 + onChange={this.handleChange}
64 + />
65 + {' '}{title}
66 + </label>
67 + </h2>
68 +
69 + <dl className="test-case__details">
70 + {resolvedIn && (
71 + <dt>First supported in: </dt>)}
72 + {resolvedIn && (
73 + <dd>
74 + <a href={'https://github.com/facebook/react/tag/v' + resolvedIn}>
75 + <code>{resolvedIn}</code>
76 + </a>
77 + </dd>
78 + )}
79 +
80 + {resolvedBy && (
81 + <dt>Fixed by: </dt>)}
82 + {resolvedBy && (
83 + <dd>
84 + <a href={'https://github.com/facebook/react/pull/' + resolvedBy.slice(1)}>
85 + <code>{resolvedBy}</code>
86 + </a>
87 + </dd>
88 + )}
89 +
90 + {affectedBrowsers &&
91 + <dt>Affected browsers: </dt>}
92 + {affectedBrowsers &&
93 + <dd>{affectedBrowsers}</dd>
94 + }
95 + </dl>
96 +
97 + <p className="test-case__desc">
98 + {description}
99 + </p>
100 +
101 + <div className="test-case__body">
102 + {!isTestRelevant &&(
103 + <p className="test-case__invalid-version">
104 + <strong>Note:</strong> This test case was fixed in a later version of React.
105 + This test is not expected to pass for the selected version, and that's ok!
106 + </p>
107 + )}
108 +
109 + {children}
110 + </div>
111 + </section>
112 + );
113 + }
114 +}
115 +
116 +TestCase.propTypes = propTypes;
117 +
118 +TestCase.Steps = class extends React.Component {
119 + render() {
120 + const { children } = this.props;
121 + return (
122 + <div>
123 + <h3>Steps to reproduce:</h3>
124 + <ol>
125 + {children}
126 + </ol>
127 + </div>
128 + )
129 + }
130 +}
131 +
132 +TestCase.ExpectedResult = class extends React.Component {
133 + render() {
134 + const { children } = this.props
135 + return (
136 + <div>
137 + <h3>Expected Result:</h3>
138 + <p>
139 + {children}
140 + </p>
141 + </div>
142 + )
143 + }
144 +}
145 +export default TestCase
fixtures/dom/src/components/fixtures/index.js
+4 -1
@@ -2,7 +2,8 @@ const React = window.React;
2 import RangeInputFixtures from './range-inputs';
3 import TextInputFixtures from './text-inputs';
4 import SelectFixtures from './selects';
5 -import TextAreaFixtures from './textareas/';
5 +import TextAreaFixtures from './textareas';
6 +import InputChangeEvents from './input-change-events';
7
8 /**
9 * A simple routing component that renders the appropriate
@@ -19,6 +20,8 @@ const FixturesPage = React.createClass({
20 return <SelectFixtures />;
21 case '/textareas':
22 return <TextAreaFixtures />;
23 + case '/input-change-events':
24 + return <InputChangeEvents />;
25 default:
26 return <p>Please select a test fixture.</p>;
27 }
fixtures/dom/src/components/fixtures/input-change-events/InputPlaceholderFixture.js new
+61
@@ -0,0 +1,61 @@
1 +import React from 'react';
2 +
3 +import Fixture from '../../Fixture';
4 +
5 +
6 +
7 +class InputPlaceholderFixture extends React.Component {
8 + constructor(props, context) {
9 + super(props, context);
10 +
11 + this.state = {
12 + placeholder: 'A placeholder',
13 + changeCount: 0,
14 + };
15 + }
16 +
17 + handleChange = () => {
18 + this.setState(({ changeCount }) => {
19 + return {
20 + changeCount: changeCount + 1
21 + }
22 + })
23 + }
24 + handleGeneratePlaceholder = () => {
25 + this.setState({
26 + placeholder: `A placeholder: ${Math.random() * 100}`
27 + })
28 + }
29 +
30 + handleReset = () => {
31 + this.setState({
32 + changeCount: 0,
33 + })
34 + }
35 +
36 + render() {
37 + const { placeholder, changeCount } = this.state;
38 + const color = changeCount === 0 ? 'green' : 'red';
39 +
40 + return (
41 + <Fixture>
42 + <input
43 + type='text'
44 + placeholder={placeholder}
45 + onChange={this.handleChange}
46 + />
47 + {' '}
48 + <button onClick={this.handleGeneratePlaceholder}>
49 + Change placeholder
50 + </button>
51 +
52 + <p style={{ color }}>
53 + <code>onChange</code>{' calls: '}<strong>{changeCount}</strong>
54 + </p>
55 + <button onClick={this.handleReset}>Reset count</button>
56 + </Fixture>
57 + )
58 + }
59 +}
60 +
61 +export default InputPlaceholderFixture;
fixtures/dom/src/components/fixtures/input-change-events/RadioClickFixture.js new
+52
@@ -0,0 +1,52 @@
1 +import React from 'react';
2 +
3 +import Fixture from '../../Fixture';
4 +
5 +class RadioClickFixture extends React.Component {
6 + constructor(props, context) {
7 + super(props, context);
8 +
9 + this.state = {
10 + changeCount: 0,
11 + };
12 + }
13 +
14 + handleChange = () => {
15 + this.setState(({ changeCount }) => {
16 + return {
17 + changeCount: changeCount + 1
18 + }
19 + })
20 + }
21 +
22 + handleReset = () => {
23 + this.setState({
24 + changeCount: 0,
25 + })
26 + }
27 +
28 + render() {
29 + const { changeCount } = this.state;
30 + const color = changeCount === 0 ? 'green' : 'red';
31 +
32 + return (
33 + <Fixture>
34 + <label>
35 + <input
36 + defaultChecked
37 + type='radio'
38 + onChange={this.handleChange}
39 + />
40 + Test case radio input
41 + </label>
42 + {' '}
43 + <p style={{ color }}>
44 + <code>onChange</code>{' calls: '}<strong>{changeCount}</strong>
45 + </p>
46 + <button onClick={this.handleReset}>Reset count</button>
47 + </Fixture>
48 + )
49 + }
50 +}
51 +
52 +export default RadioClickFixture;
fixtures/dom/src/components/fixtures/input-change-events/RangeKeyboardFixture.js new
+75
@@ -0,0 +1,75 @@
1 +import React from 'react';
2 +
3 +import Fixture from '../../Fixture';
4 +
5 +
6 +class RangeKeyboardFixture extends React.Component {
7 + constructor(props, context) {
8 + super(props, context);
9 +
10 + this.state = {
11 + keydownCount: 0,
12 + changeCount: 0,
13 + };
14 + }
15 +
16 + componentDidMount() {
17 + this.input.addEventListener('keydown', this.handleKeydown, false)
18 + }
19 +
20 + componentWillUnmount() {
21 + this.input.removeEventListener('keydown', this.handleKeydown, false)
22 + }
23 +
24 + handleChange = () => {
25 + this.setState(({ changeCount }) => {
26 + return {
27 + changeCount: changeCount + 1
28 + }
29 + })
30 + }
31 +
32 + handleKeydown = (e) => {
33 + // only interesting in arrow key events
34 + if (![37, 38, 39, 40].includes(e.keyCode))
35 + return;
36 +
37 + this.setState(({ keydownCount }) => {
38 + return {
39 + keydownCount: keydownCount + 1
40 + }
41 + })
42 + }
43 +
44 + handleReset = () => {
45 + this.setState({
46 + keydownCount: 0,
47 + changeCount: 0,
48 + })
49 + }
50 +
51 + render() {
52 + const { keydownCount, changeCount } = this.state;
53 + const color = keydownCount === changeCount ? 'green' : 'red';
54 +
55 + return (
56 + <Fixture>
57 + <input
58 + type='range'
59 + ref={r => this.input = r}
60 + onChange={this.handleChange}
61 + />
62 + {' '}
63 +
64 + <p style={{ color }}>
65 + <code>onKeyDown</code>{' calls: '}<strong>{keydownCount}</strong>
66 + {' vs '}
67 + <code>onChange</code>{' calls: '}<strong>{changeCount}</strong>
68 + </p>
69 + <button onClick={this.handleReset}>Reset counts</button>
70 + </Fixture>
71 + )
72 + }
73 +}
74 +
75 +export default RangeKeyboardFixture;
fixtures/dom/src/components/fixtures/input-change-events/index.js new
+81
@@ -0,0 +1,81 @@
1 +import React from 'react';
2 +
3 +import FixtureSet from '../../FixtureSet';
4 +import TestCase from '../../TestCase';
5 +import RangeKeyboardFixture from './RangeKeyboardFixture';
6 +import RadioClickFixture from './RadioClickFixture';
7 +import InputPlaceholderFixture from './InputPlaceholderFixture';
8 +
9 +class InputChangeEvents extends React.Component {
10 + render() {
11 + return (
12 + <FixtureSet
13 + title="Input change events"
14 + description="Tests proper behavior of the onChange event for inputs"
15 + >
16 + <TestCase
17 + title="Range keyboard changes"
18 + description={`
19 + Range inputs should fire onChange events for keyboard events
20 + `}
21 + >
22 + <TestCase.Steps>
23 + <li>Focus range input</li>
24 + <li>change value via the keyboard arrow keys</li>
25 + </TestCase.Steps>
26 +
27 + <TestCase.ExpectedResult>
28 + The <code>onKeyDown</code> call count should be equal to
29 + the <code>onChange</code> call count.
30 + </TestCase.ExpectedResult>
31 +
32 + <RangeKeyboardFixture />
33 + </TestCase>
34 +
35 + <TestCase
36 + title="Radio input clicks"
37 + description={`
38 + Radio inputs should only fire change events when the checked
39 + state changes.
40 + `}
41 + resolvedIn="16.0.0"
42 + >
43 + <TestCase.Steps>
44 + <li>Click on the Radio input (or label text)</li>
45 + </TestCase.Steps>
46 +
47 + <TestCase.ExpectedResult>
48 + The <code>onChange</code> call count should remain at 0
49 + </TestCase.ExpectedResult>
50 +
51 + <RadioClickFixture />
52 + </TestCase>
53 +
54 + <TestCase
55 + title="Inputs with placeholders"
56 + description={`
57 + Text inputs with placeholders should not trigger changes
58 + when the placeholder is altered
59 + `}
60 + resolvedIn="15.0.0"
61 + resolvedBy="#5004"
62 + affectedBrowsers="IE9+"
63 + >
64 + <TestCase.Steps>
65 + <li>Click on the Text input</li>
66 + <li>Click on the "Change placeholder" button</li>
67 + </TestCase.Steps>
68 +
69 + <TestCase.ExpectedResult>
70 + The <code>onChange</code> call count should remain at 0
71 + </TestCase.ExpectedResult>
72 +
73 + <InputPlaceholderFixture />
74 + </TestCase>
75 + </FixtureSet>
76 + );
77 + }
78 +}
79 +
80 +
81 +export default InputChangeEvents
fixtures/dom/src/components/fixtures/text-inputs/index.js
+2 -2
@@ -24,7 +24,7 @@ const TextInputFixtures = React.createClass({
24
25 return (
26 <div key={type} className="field">
27 - <label htmlFor={id}>{type}</label>
27 + <label className="control-label" htmlFor={id}>{type}</label>
28 <input id={id} type={type} value={state} onChange={onChange} />
29 &nbsp; &rarr; {JSON.stringify(state)}
30 </div>
@@ -35,7 +35,7 @@ const TextInputFixtures = React.createClass({
35 let id = `uncontrolled_${type}`;
36 return (
37 <div key={type} className="field">
38 - <label htmlFor={id}>{type}</label>
38 + <label className="control-label" htmlFor={id}>{type}</label>
39 <input id={id} type={type} />
40 </div>
41 );
fixtures/dom/src/components/propTypes.js new
+16
@@ -0,0 +1,16 @@
1 +import semver from 'semver';
2 +
3 +const React = window.React;
4 +
5 +export function semverString (props, propName, componentName) {
6 + let version = props[propName];
7 +
8 + let error = React.PropTypes.string(...arguments);
9 + if (!error && version != null && !semver.valid(version))
10 + error = new Error(
11 + `\`${propName}\` should be a valid "semantic version" matching ` +
12 + 'an existing React version'
13 + );
14 +
15 + return error || null;
16 +};
fixtures/dom/src/style.css
+134 -10
@@ -4,22 +4,28 @@
4 box-sizing: border-box;
5 }
6
7 +html {
8 + font-size: 10px;
9 +}
10 body {
11 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
12 + font-size: 1.4rem;
13 margin: 0;
14 padding: 0;
15 }
16
17 select {
14 - width: 120px;
18 + width: 12rem;
19 }
20
21 +
22 .header {
23 background: #222;
24 box-shadow: inset 0 -1px 3px #000;
20 - line-height: 32px;
25 + font-size: 1.6rem;
26 + line-height: 3.2rem;
27 overflow: hidden;
22 - padding: 8px 16px;
28 + padding: .8rem 1.6rem;
29 }
30
31 .header__inner {
@@ -40,7 +46,7 @@ select {
46
47 .header__logo img {
48 display: inline-block;
43 - margin-right: 8px;
49 + margin-right: 0.8rem;
50 vertical-align: middle;
51 }
52
@@ -63,24 +69,142 @@ select {
69 margin: 0 auto;
70 max-width: 900px;
71 overflow: hidden;
66 - padding: 20px;
72 + padding: 2rem;
73 }
74
69 -label {
75 +.control-label {
76 display: block;
71 - font-size: 12px;
77 + font-size: 1.2rem;
78 letter-spacing: 0.01em;
73 - margin-bottom: 4px;
79 + margin-bottom: 0.4rem;
80 text-transform: uppercase;
81 }
82
83 .field {
78 - padding: 8px;
84 + padding: 0.8rem;
85 }
86
87 fieldset {
88 border: 1px solid #aaa;
89 float: left;
84 - padding: 16px;
90 + padding: 1.6rem;
91 width: 49%;
92 }
93 +
94 +.control-box {
95 + overflow: hidden;
96 +}
97 +
98 +ul, ol {
99 + margin: 0 0 2rem 0;
100 +}
101 +
102 +li {
103 + margin-bottom: 0.4rem;
104 +}
105 +
106 +.type-subheading {
107 + font-size: 1.8rem;
108 + font-weight: 600;
109 + line-height: 1.5;
110 + margin: 0 0 1.6rem;
111 +}
112 +
113 +.hint {
114 + font-style: italic;
115 + line-height: 1.5;
116 + text-size: 1.4rem;
117 +}
118 +
119 +.footnote {
120 + border-left: 4px solid #aaa;
121 + color: #444;
122 + font-style: italic;
123 + line-height: 1.5;
124 + margin-bottom: 2.4rem;
125 + margin-left: 0.4rem;
126 + padding-left: 1.6rem;
127 + text-size: 1.3rem;
128 +}
129 +
130 +.test-case {
131 + border-radius: 0.2rem;
132 + border: 1px solid #d9d9d9;
133 + margin: 3.2rem 0 3.2rem;
134 +}
135 +
136 +.test-case__title {
137 + padding: 10px 15px 8px;
138 + line-height: 16px;
139 + font-size: 18px;
140 + border-bottom: 1px dashed #d9d9d9;
141 + margin: 0 0 -1px;
142 +}
143 +
144 +.test-case__title__check {
145 + display: inline-block;
146 + margin-right: 8px;
147 + vertical-align: middle;
148 +}
149 +
150 +.test-case__body {
151 + padding: 0 15px;
152 +}
153 +
154 +.test-case__desc {
155 + font-style: italic;
156 + margin: 15px 0;
157 + padding: 0 15px;
158 +}
159 +
160 +.test-case--complete {
161 + border-color: #5cb85c;
162 +}
163 +
164 +.test-case--complete .test-case__title {
165 + color: #5cb85c;
166 +}
167 +
168 +.test-case__details {
169 + border-bottom: 1px dashed #d9d9d9;
170 + font-size: 80%;
171 + text-transform: uppercase;
172 + letter-spacing: 0.02em;
173 + margin: 0;
174 + padding: 0 15px;
175 +}
176 +
177 +.test-case__details > * {
178 + display: inline-block;
179 +}
180 +
181 +.test-case__details > dt,
182 +.test-case__details > dd {
183 + padding: 8px 0 6px;
184 +}
185 +
186 +.test-case__details > dt {
187 + color: #464a4c;
188 + font-weight: 600;
189 +}
190 +
191 +.test-case__details > dd {
192 + margin-left: 1rem;
193 +}
194 +
195 +.test-case__details > dd + dt {
196 + margin-left: 1.5rem;
197 +}
198 +
199 +.test-case__invalid-version {
200 + font-style: italic;
201 + font-size: 1.6rem;
202 + color: #5cb85c;
203 +}
204 +
205 +.test-fixture {
206 + padding: 20px;
207 + margin: 0 -15px; /* opposite of .test-case padding */
208 + background-color: #f4f4f4;
209 + border-top: 1px solid #d9d9d9;
210 +}
fixtures/dom/src/tags.js new
+70
@@ -0,0 +1,70 @@
1 +/**
2 + * Version tags are loaded from the Github API. Since the Github API is rate-limited
3 + * we attempt to save and load the tags in sessionStorage when possible. Since its unlikely
4 + * that versions will change during a single session this should be safe.
5 + */
6 +
7 +const TAGS_CACHE_KEY = '@react-dom-fixtures/tags';
8 +
9 +/**
10 + * Its possible that users will be testing changes frequently
11 + * in a browser that does not support sessionStorage. If the API does
12 + * get rate limited this hardcoded fallback will be loaded instead.
13 + * This way users can still switch between ~some versions while testing.
14 + * If there's a specific version they need to test that is not here, they
15 + * can manually load it by editing the URL (`?version={whatever}`)
16 + */
17 +const fallbackTags = [
18 + '15.4.2',
19 + '15.3.2',
20 + '15.2.1',
21 + '15.1.0',
22 + '15.0.2',
23 + '0.14.8',
24 + '0.13.0'
25 + ].map(version => ({
26 + name: `v${version}`
27 + }))
28 +
29 +let canUseSessionStorage = true;
30 +
31 +try {
32 + sessionStorage.setItem('foo', '')
33 +} catch (err) {
34 + canUseSessionStorage = false;
35 +}
36 +
37 +/**
38 + * Attempts to load tags from sessionStorage. In cases where
39 + * sessionStorage is not available (Safari private browsing) or the
40 + * tags are cached a fetch request is made to the Github API.
41 + *
42 + * Returns a promise so that the consuming module can always assume
43 + * the request is async, even if its loaded from sessionStorage.
44 + */
45 +export default function getVersionTags() {
46 + return new Promise((resolve) => {
47 + let cachedTags;
48 + if (canUseSessionStorage) {
49 + cachedTags = sessionStorage.getItem(TAGS_CACHE_KEY);
50 + }
51 + if (cachedTags) {
52 + cachedTags = JSON.parse(cachedTags);
53 + resolve(cachedTags);
54 + } else {
55 + fetch('https://api.github.com/repos/facebook/react/tags', { mode: 'cors' })
56 + .then(res => res.json())
57 + .then(tags => {
58 + // A message property indicates an error was sent from the API
59 + if (tags.message) {
60 + return resolve(fallbackTags)
61 + }
62 + if (canUseSessionStorage) {
63 + sessionStorage.setItem(TAGS_CACHE_KEY, JSON.stringify(tags))
64 + }
65 + resolve(tags)
66 + })
67 + .catch(() => resolve(fallbackTags))
68 + }
69 + })
70 +}
\ No newline at end of file
fixtures/dom/yarn.lock
+4
@@ -1136,6 +1136,10 @@ clap@^1.0.9:
1136 dependencies:
1137 chalk "^1.1.3"
1138
1139 +classnames@^2.2.5:
1140 + version "2.2.5"
1141 + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
1142 +
1143 clean-css@3.4.x:
1144 version "3.4.23"
1145 resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.23.tgz#604fbbca24c12feb59b02f00b84f1fb7ded6d001"