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
→ {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"