Skip to content

Commit e96e471

Browse files
authored
docs: draft for the various click scenarios doc (#2218)
1 parent f63ea3f commit e96e471

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

docs/development/click.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
## Supported click scenarios
2+
3+
These are some clicking corner cases that we did consider and decided to support.
4+
5+
### Positioning
6+
7+
- Element is outside of the viewport.
8+
```html
9+
<div style="height: 2000px;">Some content</div>
10+
<button>Click me</button>
11+
```
12+
13+
We use `scrollRectIntoViewIfNeeded` to scroll the element into the viewport if at all possible.
14+
15+
- Empty element with non-empty pseudo.
16+
17+
```html
18+
<style>span::before { content: 'q'; }</style>
19+
<span></span>
20+
```
21+
22+
We retrieve the actual visible regions of the target element and click at the pseudo.
23+
24+
- Some part of the element is always outside of the viewport.
25+
26+
```html
27+
<style> i { position: absolute; top: -1000px; } </style>
28+
<span><i>one</i><b>two</b></span>
29+
```
30+
31+
We retrieve the actual visible regions of the target element and click at the visible part.
32+
33+
- Inline element is wrapped to the next line.
34+
35+
We retrieve the actual visible regions of the target element and click at one of the inline boxes.
36+
37+
- Element is rotated with transform.
38+
39+
```html
40+
<button style="transform: rotate(50deg);">Click me</button>
41+
```
42+
43+
We retrieve the actual visible regions of the target element and click at the transformed visible point.
44+
45+
- Element is deep inside the iframes and/or shadow dom.
46+
47+
We click it.
48+
49+
### Dynamic changes
50+
51+
- Element appears dynamically using display or visibility.
52+
```html
53+
<button style="display: none">Click me</button>
54+
<script>
55+
setTimeout(() => document.querySelector('button').style.display = 'inline', 5000);
56+
</script>
57+
```
58+
59+
We wait for the element to be visible before clicking.
60+
61+
- Element is animating in.
62+
63+
```html
64+
<style>
65+
@keyframes move { from { marign-left: 0; } to { margin-left: 100px; } }
66+
</style>
67+
<button style="animation: 3s linear move forwards;">Click me</button>
68+
```
69+
70+
We wait for the element to stop moving before clicking.
71+
72+
- Another element is temporary obscuring the target element.
73+
74+
```html
75+
<style>
76+
.overlay {
77+
position: absolute;
78+
left: 0; top: 0; right: 0; bottom: 0;
79+
background: rgba(128, 128, 128, 0.5);
80+
transition: opacity 1s;
81+
}
82+
</style>
83+
<div style="position: relative;">
84+
<button>Click me</button>
85+
<div class=overlay></div>
86+
</div>
87+
<script>
88+
const div = document.querySelector('.overlay');
89+
div.addEventListener('click', () => {
90+
div.style.opacity ='0';
91+
setTimeout(() => { div.remove(); }, 1000);
92+
});
93+
</script>
94+
```
95+
96+
For example, the dialog is dismissed and is slowly fading out. We wait for the obscuring element to disappear.
97+
More precisely, we wait for the target element to actually receive pointer events.
98+
99+
- Element is replaced with another one after animation.
100+
101+
```html
102+
<style>
103+
@keyframes move { from { marign-left: 0; } to { margin-left: 100px; } }
104+
</style>
105+
<button style="animation: 3s linear move forwards;">Click me</button>
106+
<script>
107+
setTimeout(() => {
108+
const button = document.createElement('button');
109+
button.textContent = 'Click me';
110+
document.querySelector('button').replaceWith(button);
111+
}, 2500);
112+
</script>
113+
```
114+
115+
We wait for the element to be at a stable position, detect that it has been removed from the DOM and retry.
116+
117+
### Targeting
118+
119+
- Button with span/label inside that has `pointer-events: none`.
120+
121+
```html
122+
<button>
123+
<label style="pointer-events:none">Click target</label>
124+
</button>
125+
```
126+
127+
We assume that in such a case the first parent receiving pointer events is a click target.
128+
This is very convenient with something like `text=Click target` selector that actually targets the inner element.
129+
130+
131+
## Unsupported click scenarios
132+
133+
These are some clicking corner cases that we considered.
134+
135+
Some scenarios are marked as a bug in the web page - we believe that the page should be fixed because the real user will suffer the same issue. We try to throw when it's possible to detect the issue or timeout otherwise.
136+
137+
Other scenarios are perfectly fine, but we cannot support them, and usually suggest another way to handle. If Playwright logic does not work on your page, passing `{force: true}` option to the click will force the click without any checks. Use it when you know that's what you need.
138+
139+
### Positioning
140+
141+
- Element moves outside of the viewport in onscroll.
142+
143+
```html
144+
<div style="height: 2000px;">Some content</div>
145+
<button>Click me</button>
146+
<script>
147+
window.addEventListener('scroll', () => {
148+
window.h = (window.h || 2000) + 200;
149+
document.querySelector('div').style.height = window.h + 'px';
150+
});
151+
</script>
152+
```
153+
154+
We consider this a bug in the page and throw.
155+
156+
### Dynamic changes
157+
158+
- Element is constantly animating.
159+
160+
```html
161+
<style>
162+
@keyframes move { from { marign-left: 0; } to { margin-left: 100px; } }
163+
200px; } }
164+
</style>
165+
<button style="animation: 3s linear move infinite;">Click me</button>
166+
```
167+
168+
We wait for the element to be at a stable position and timeout. A real user would be able to click in some cases.
169+
170+
- Element is animating in, but temporarily pauses in the middle.
171+
172+
```html
173+
<style>
174+
@keyframes move { 0% { marign-left: 0; } 25% { margin-left: 100px; } 50% { margin-left: 100px;} 100% { margin-left: 200px; } }
175+
</style>
176+
<button style="animation: 3s linear move forwards;">Click me</button>
177+
```
178+
179+
We click in the middle of the animation and could actually click at the wrong element. We do not detect this case and do not throw. A real user would probably retry and click again.
180+
181+
- Element is removed or hidden after `fetch` / `xhr` / `setTimeout`.
182+
183+
```html
184+
<button>Click me</button>
185+
<script>
186+
fetch(location.href).then(() => document.querySelector('button').remove());
187+
</script>
188+
```
189+
190+
We click the element and might be able to misclick. We do not detect this case and do not throw.
191+
192+
This is a typical flaky failure, because the network fetch is racing against the input driven by Playwright. The suggested solution is to wait for the response to arrive, and only then click. For example, consider a filtered list with a "Apply filters" button that fetches new data, removes all items from the list and inserts new ones.
193+
194+
```js
195+
await Promise.all([
196+
// This click triggers network fetch racing with next click.
197+
page.click('text=Apply filters'),
198+
// This waits for the network response to arrive.
199+
page.waitForResponse('**/filtered?*'),
200+
]);
201+
// Safe to click now, because network response has been processed
202+
// and items in the list have been updated.
203+
await page.click('.list-item');
204+
```
205+
206+
207+
### Targeting
208+
209+
- A transparent overlay handles the input targeted at the content behind it.
210+
211+
```html
212+
<div style="position: relative;">
213+
<span>Click me</span>
214+
<div style="position: absolute; left: 0; top: 0; right: 0; bottom: 0" onclick="..."></div>
215+
</div>
216+
```
217+
218+
We consider the overlay temporary and timeout waiting for it to disappear.
219+
When the overlay element is actually handling the input instead of the target element, use `{force: true}` option to skip the checks and click anyway.
220+
221+
- Hover handler creates an overlay.
222+
223+
```html
224+
<style>
225+
.overlay { display: none; }
226+
.container:hover > .overlay { display: block; }
227+
</style>
228+
<div class=container style="position: relative;">
229+
<button>Click me</button>
230+
<div class=overlay style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: red"></div>
231+
</div>
232+
```
233+
234+
We consider this a bug in the page, because in most circumstances users will not be able to click the element.
235+
When the overlay element is actually handling the input instead of the target element, use `{force: true}` option to skip the checks and click anyway.
236+
237+
- `pointer-events` changes dynamically.
238+
239+
```html
240+
<button style="pointer-events: none">Click me</button>
241+
<script>
242+
setTimeout(() => document.querySelector('button').style.pointerEvents = 'auto', 5000);
243+
</script>
244+
```
245+
246+
We consider this a bug in the page, because users will not be able to click the element when they see it.
247+

0 commit comments

Comments
 (0)