Skip to content

Commit 3079cd2

Browse files
committed
Create DOM fixture that tests state preservation of timed out content
1 parent 5e3dc2e commit 3079cd2

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed

fixtures/dom/src/components/Header.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class Header extends React.Component {
6868
<option value="/pointer-events">Pointer Events</option>
6969
<option value="/mouse-events">Mouse Events</option>
7070
<option value="/selection-events">Selection Events</option>
71+
<option value="/suspense">Suspense</option>
7172
</select>
7273
</label>
7374
<label htmlFor="react_version">
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import Fixture from '../../Fixture';
2+
import FixtureSet from '../../FixtureSet';
3+
import TestCase from '../../TestCase';
4+
5+
const React = window.React;
6+
const ReactDOM = window.ReactDOM;
7+
8+
const Suspense = React.unstable_Suspense;
9+
10+
let cache = new Set();
11+
12+
function AsyncStep({text, ms}) {
13+
if (!cache.has(text)) {
14+
throw new Promise(resolve =>
15+
setTimeout(() => {
16+
cache.add(text);
17+
resolve();
18+
}, ms)
19+
);
20+
}
21+
return null;
22+
}
23+
24+
let suspendyTreeIdCounter = 0;
25+
class SuspendyTreeChild extends React.Component {
26+
id = suspendyTreeIdCounter++;
27+
state = {
28+
step: 1,
29+
isHidden: false,
30+
};
31+
increment = () => this.setState(s => ({step: s.step + 1}));
32+
33+
componentDidMount() {
34+
document.addEventListener('keydown', this.onKeydown);
35+
}
36+
37+
componentWillUnmount() {
38+
document.removeEventListener('keydown', this.onKeydown);
39+
}
40+
41+
onKeydown = event => {
42+
if (event.metaKey && event.key === 'Enter') {
43+
this.increment();
44+
}
45+
};
46+
47+
render() {
48+
return (
49+
<React.Fragment>
50+
<Suspense fallback={<div>(display: none)</div>}>
51+
<div>
52+
<AsyncStep text={`${this.state.step} + ${this.id}`} ms={500} />
53+
{this.props.children}
54+
</div>
55+
</Suspense>
56+
<button onClick={this.increment}>Hide</button>
57+
</React.Fragment>
58+
);
59+
}
60+
}
61+
62+
class SuspendyTree extends React.Component {
63+
parentContainer = React.createRef(null);
64+
container = React.createRef(null);
65+
componentDidMount() {
66+
this.setState({});
67+
document.addEventListener('keydown', this.onKeydown);
68+
}
69+
componentWillUnmount() {
70+
document.removeEventListener('keydown', this.onKeydown);
71+
}
72+
onKeydown = event => {
73+
if (event.metaKey && event.key === '/') {
74+
this.removeAndRestore();
75+
}
76+
};
77+
removeAndRestore = () => {
78+
const parentContainer = this.parentContainer.current;
79+
const container = this.container.current;
80+
parentContainer.removeChild(container);
81+
parentContainer.textContent = '(removed from DOM)';
82+
setTimeout(() => {
83+
parentContainer.textContent = '';
84+
parentContainer.appendChild(container);
85+
}, 500);
86+
};
87+
render() {
88+
return (
89+
<React.Fragment>
90+
<div ref={this.parentContainer}>
91+
<div ref={this.container} />
92+
</div>
93+
<div>
94+
{this.container.current !== null
95+
? ReactDOM.createPortal(
96+
<React.Fragment>
97+
<SuspendyTreeChild>{this.props.children}</SuspendyTreeChild>
98+
<button onClick={this.removeAndRestore}>Remove</button>
99+
</React.Fragment>,
100+
this.container.current
101+
)
102+
: null}
103+
</div>
104+
</React.Fragment>
105+
);
106+
}
107+
}
108+
109+
class TextInputFixtures extends React.Component {
110+
render() {
111+
return (
112+
<FixtureSet
113+
title="Suspense"
114+
description="Preserving the state of timed-out children">
115+
<p>
116+
Clicking "Hide" will hide the fixture context using{' '}
117+
<code>display: none</code> for 0.5 seconds, then restore. This is the
118+
built-in behavior for timed-out children. Each fixture tests whether
119+
the state of the DOM is preserved. Clicking "Remove" will remove the
120+
fixture content from the DOM for 0.5 seconds, then restore. This is{' '}
121+
<strong>not</strong> how timed-out children are hidden, but is
122+
included for comparison purposes.
123+
</p>
124+
<div className="footnote">
125+
As a shortcut, you can use Command + Enter (or Control + Enter on
126+
Windows, Linux) to "Hide" all the fixtures, or Command + / to "Remove"
127+
them.
128+
</div>
129+
<TestCase title="Text selection where entire range times out">
130+
<TestCase.Steps>
131+
<li>Use your cursor to select the text below.</li>
132+
<li>Click "Hide" or "Remove".</li>
133+
</TestCase.Steps>
134+
135+
<TestCase.ExpectedResult>
136+
Text selection is preserved when hiding, but not when removing.
137+
</TestCase.ExpectedResult>
138+
139+
<Fixture>
140+
<SuspendyTree>
141+
Select this entire sentence (and only this sentence).
142+
</SuspendyTree>
143+
</Fixture>
144+
</TestCase>
145+
<TestCase title="Text selection that extends outside timed-out subtree">
146+
<TestCase.Steps>
147+
<li>
148+
Use your cursor to select a range that includes both the text and
149+
the "Go" button.
150+
</li>
151+
<li>Click "Hide" or "Remove".</li>
152+
</TestCase.Steps>
153+
154+
<TestCase.ExpectedResult>
155+
Text selection is preserved when hiding, but not when removing.
156+
</TestCase.ExpectedResult>
157+
158+
<Fixture>
159+
<SuspendyTree>
160+
Select a range that includes both this sentence and the "Go"
161+
button.
162+
</SuspendyTree>
163+
</Fixture>
164+
</TestCase>
165+
<TestCase title="Focus">
166+
<TestCase.Steps>
167+
<li>
168+
Use your cursor to select a range that includes both the text and
169+
the "Go" button.
170+
</li>
171+
<li>
172+
Intead of clicking "Go", which switches focus, press Command +
173+
Enter (or Control + Enter on Windows, Linux).
174+
</li>
175+
</TestCase.Steps>
176+
177+
<TestCase.ExpectedResult>
178+
The ideal behavior is that the focus would not be lost, but
179+
currently it is (both when hiding and removing).
180+
</TestCase.ExpectedResult>
181+
182+
<Fixture>
183+
<SuspendyTree>
184+
<button>Focus me</button>
185+
</SuspendyTree>
186+
</Fixture>
187+
</TestCase>
188+
<TestCase title="Uncontrolled form input">
189+
<TestCase.Steps>
190+
<li>Type something ("Hello") into the text input.</li>
191+
<li>Click "Hide" or "Remove".</li>
192+
</TestCase.Steps>
193+
194+
<TestCase.ExpectedResult>
195+
Input is preserved when hiding, but not when removing.
196+
</TestCase.ExpectedResult>
197+
198+
<Fixture>
199+
<SuspendyTree>
200+
<input type="text" />
201+
</SuspendyTree>
202+
</Fixture>
203+
</TestCase>
204+
<TestCase title="Image flicker">
205+
<TestCase.Steps>
206+
<li>Click "Hide" or "Remove".</li>
207+
</TestCase.Steps>
208+
209+
<TestCase.ExpectedResult>
210+
The image should reappear without flickering. The text should not
211+
reflow.
212+
</TestCase.ExpectedResult>
213+
214+
<Fixture>
215+
<SuspendyTree>
216+
<img src="https://upload.wikimedia.org/wikipedia/commons/e/ee/Atom_%282%29.png" />React
217+
is cool
218+
</SuspendyTree>
219+
</Fixture>
220+
</TestCase>
221+
<TestCase title="Iframe">
222+
<TestCase.Steps>
223+
<li>
224+
The iframe shows a nested version of this fixtures app. Navigate
225+
to the "Text inputs" page.
226+
</li>
227+
<li>Select one of the checkboxes.</li>
228+
<li>Click "Hide" or "Remove".</li>
229+
</TestCase.Steps>
230+
231+
<TestCase.ExpectedResult>
232+
When removing, the iframe is reloaded. When hiding, the iframe
233+
should still be on the "Text inputs" page. The checkbox should still
234+
be checked. (Unfortunately, scroll position is lost.)
235+
</TestCase.ExpectedResult>
236+
237+
<Fixture>
238+
<SuspendyTree>
239+
<iframe width="500" height="300" src="/" />
240+
</SuspendyTree>
241+
</Fixture>
242+
</TestCase>
243+
<TestCase title="Video playback">
244+
<TestCase.Steps>
245+
<li>Start playing the video, or seek to a specific position.</li>
246+
<li>Click "Hide" or "Remove".</li>
247+
</TestCase.Steps>
248+
249+
<TestCase.ExpectedResult>
250+
The playback position should stay the same. When hiding, the video
251+
plays in the background for the entire duration. When removing, the
252+
video stops playing, but the position is not lost.
253+
</TestCase.ExpectedResult>
254+
255+
<Fixture>
256+
<SuspendyTree>
257+
<video controls>
258+
<source
259+
src="http://techslides.com/demos/sample-videos/small.webm"
260+
type="video/webm"
261+
/>
262+
<source
263+
src="http://techslides.com/demos/sample-videos/small.ogv"
264+
type="video/ogg"
265+
/>
266+
<source
267+
src="http://techslides.com/demos/sample-videos/small.mp4"
268+
type="video/mp4"
269+
/>
270+
<source
271+
src="http://techslides.com/demos/sample-videos/small.3gp"
272+
type="video/3gp"
273+
/>
274+
</video>
275+
</SuspendyTree>
276+
</Fixture>
277+
</TestCase>
278+
<TestCase title="Audio playback">
279+
<TestCase.Steps>
280+
<li>Start playing the audio, or seek to a specific position.</li>
281+
<li>Click "Hide" or "Remove".</li>
282+
</TestCase.Steps>
283+
284+
<TestCase.ExpectedResult>
285+
The playback position should stay the same. When hiding, the audio
286+
plays in the background for the entire duration. When removing, the
287+
audio stops playing, but the position is not lost.
288+
</TestCase.ExpectedResult>
289+
<Fixture>
290+
<SuspendyTree>
291+
<audio controls={true}>
292+
<source src="https://upload.wikimedia.org/wikipedia/commons/e/ec/Mozart_K448.ogg" />
293+
</audio>
294+
</SuspendyTree>
295+
</Fixture>
296+
</TestCase>
297+
<TestCase title="Scroll position">
298+
<TestCase.Steps>
299+
<li>Scroll to a position in the list.</li>
300+
<li>Click "Hide" or "Remove".</li>
301+
</TestCase.Steps>
302+
303+
<TestCase.ExpectedResult>
304+
Scroll position is preserved when hiding, but not when removing.
305+
</TestCase.ExpectedResult>
306+
<Fixture>
307+
<SuspendyTree>
308+
<div style={{height: 200, overflow: 'scroll'}}>
309+
{Array(20)
310+
.fill()
311+
.map((_, i) => <h2 key={i}>{i + 1}</h2>)}
312+
</div>
313+
</SuspendyTree>
314+
</Fixture>
315+
</TestCase>
316+
</FixtureSet>
317+
);
318+
}
319+
}
320+
321+
export default TextInputFixtures;

0 commit comments

Comments
 (0)