Het leukste aan mijn werk als devver vind ik het oplossen van problemen. Of het probleem nu is dat er iets stuk is, dat er iets gemaakt moet worden dat er nog niet is, of dat iets suboptimaal werkt; het oplossen van zulke problemen geeft mij een voldoening. Juist bugs oplossen geeft mij onwijs veel voldoening. Ik denk dat dit hetzelfde werkt als een finishlijn: Je hebt een concreet doel, weet wanneer je het bereikt hebt en moet alleen nog even 42,195 km hardlopen. Ik begon dus enthousiast te worden van deze bug. Het enige nadeel: Ik heb redelijk wat frontend kennis, maar ben zeker geen expert. En voor bugs oplossen is het juist onhandig als je redelijke kennis hebt: Je bent niet expert genoeg om alle valkuilen te kennen, maar ook niet onervaren genoeg dat je zaken goed aanleert. Toch kreeg het lot (in combinatie met mijn enthousiasme) het voor elkaar dat ik deze bug ging oppakken.
Het vervelende met deze bug is dat ie specifiek alleen voorkomt op een iPhone met Chrome. Nu zijn er relatief weinig bezoekers op Tweakers met deze combinatie, waardoor we al een beetje twijfelden hoeveel tijd we hier in zouden moeten steken. Het kan immers zijn dat het ontdekken waar ‘t probleem precies door komt al veel tijd in beslag zal nemen, en dat er een kans bestaat dat we daarna niets anders kunnen dan concluderen dat het een bug in de browser is; die we dus niet zelf kunnen verhelpen. En als we het wel oplossen na heel veel tijd te hebben geïnvesteerd, is er maar een kleine groep die er iets van zal merken.
Tijdens de aftrap van de sprint hadden we als team dan ook besloten om deze bug niet koste wat kost moeten oplossen, maar dat we er maximaal 1 werkdag aan zouden besteden. Als we na die ene dag geen zicht hadden op een oplossing, zouden we het in de ijskast zetten en onze aandacht richten op “belangrijkere” zaken. Dat klinkt wellicht hard, maar devtijd is helaas nu eenmaal schaars.
Ik had voordat ik aan de slag ging al wel een vermoeden waar ‘t probleem zou liggen. We konden het immers consequent reproduceren in Chrome op een iPhone nadat we een stukje hadden gescrold. Dit speelde niet op andere websites, en speelde niet in de Safaribrowser zelf
1 . Ik dacht daarom dat het een probleem zou zijn met onze JavaScript die “iets” doet met scrollen.
Op onze testomgeving had ik daarom een kopie van de website klaargezet, waar ik alle JavaScript uit had gestript. Een snelle controle met de iPhone van een van de andere devvers gaf aan dat daar het probleem inderdaad niet speelde, en dus onze JavaScript (onderdeel van) het probleem is. Ik was er toen van overtuigd dat ik het probleem wel zou kunnen vinden.
Alle devvers draaien op hun eigen machine een kopie van de website, en kunnen dus ook lokaal de website testen. We kunnen hierdoor heel snel kleine wijzigingen checken, omdat je niet hoeft te wachten op een-of-ander onuitspreekbaar cluster
2 om een container op te spinnen. Omdat ik zelf Linux draai, en het debuggen van een iPhone een Mac vereist (en bovendien eerder op Safari gericht is dan Chrome) voorzag ik dat ik heel vaak een kleine wijziging zou moeten controleren. Het is dus cruciaal dat de tijd tussen een wijziging en deze kunnen controleren zo klein mogelijk is; anders haal ik het nooit om het in 1 werkdag op te lossen. Oftewel, ik moest de lokale omgeving opvragen via een iPhone. Maar deze lokale omgeving is – zoals ‘t woord al aangeeft – lokaal (127.0.0.1), en dat vereist wat veranderingen wil ik dat via een iPhone kunnen opvragen.
Normaal zou ik een SSH tunnel opzetten om verkeer te kunnen rerouten, maar omdat ik met een iPhone zou moeten gaan testen (die wellicht ook buiten ‘t netwerk zit) leek het mij beter om een “echt” domein te gebruiken, met een proxy die dat verkeer dan naar mijn lokale omgeving doorstuurde. Dus ik ging aan de slag met ngrok, en nadat ik mijn tweakers omgeving had ingesteld op de ngrok “publieke URL”
3 kon ik dan echt aan de slag.
Goed, mijn vermoeden was dat het probleem werd veroorzaakt door JavaScript dat iets doet met scrollen. Het idee dat ik had is dat onze JavaScript iets doet op het moment dat je de vinger “neerlaat” op het apparaat, waardoor het “loslaten” van de vinger niet meer wordt geregistreerd. De eerstvolgende keer dat je klikt “reset” deze state zich dan weer, waarna het klikken weer werkt. Ik deed dus een grep over de codebase om te zien waar we event listeners gebruiken, en had deze tijdelijk uitgezet. Het probleem speelde niet, totdat ik die event listeners weer aanzette. Op deze manier kon ik door verschillende delen in- of uit te schakelen precies zien welk deel van onze JavaScript het probleem veroorzaakte: ScrollArrows.
ScrollArrows is de naam van de module in onze JavaScript voor het pijltje waarmee je snel naar ‘t einde van de pagina kunt scrollen. Dit pijltje verschijnt pas als je zelf wat naar beneden hebt gescrold, en verdwijnt automatisch na een tijdje wanneer je gestopt bent met scrollen.
Blijkbaar zorgt dit ding er voor dat je twee keer moet klikken. En wat mij toen opviel: Zo lang dit pijltje zichtbaar is, speelt het probleem. Maar wacht je lang genoeg na het scrollen totdat dat pijltje weg is, dan speelt ‘t probleem niet meer. Op dat moment ging mijn rechter wenkbrauw omhoog, da’s interessant!
:fill(white):strip_exif()/f/image/GEHQzxexgNi9DsbP22Bzurbc.png?f=user_large)
(click to play)
Zo lang de achtergrond blauw is, hoef je maar eenmaal te klikken op een link. Is de achtergrond rood, moet je tweemaal klikken. Om de een of andere manier deed mij dit denken aan Annamaria koekoek. Die zonnebril komt trouwens goed van pas met zulke frontendskills.
Daarna volgde een saai proces van het proberen te reduceren van de hoeveelheid code om de minimale hoeveelheid over te houden om de bug te kunnen reproduceren. Tijdens dit proces viel mijn oog ineens op een methode genaamd ClickBuster. Dit is een module die touch events afvangt om kliks te voorkomen op het moment dat je iets met touch doet en dan per ongeluk ergens op zou klikken. Even dacht ik dat dit met mijn bug te maken heeft, maar dat bleek (helaas) niet het geval.
Uiteindelijk hield ik zo’n kleine hoeveelheid HTML, CSS en JavaScript over dat ik niet anders kan concluderen dat het inderdaad een browserbug is:
HTML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
| <!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width">
<title>iOS/Chrome double click after scroll bug</title>
<style>
/*
* This background-color is not required for reproducing the bug,
* but only is used to give feedback to the user. As long as the
* background is blue, a single click on the link should be
* registered.
* A red background indicates that the #scollTo element is displayed,
* that is when clicking the link requires an additional click.
*/
#layout {
background-color: blue;
}
#layout.show {
background-color: red;
}
#scrollTo {
opacity: 0;
position: fixed;
}
#layout.show #scrollTo {
opacity: 1;
}
</style>
</head>
<body>
<div id="layout">
<p>
Minimal required HTML, CSS and JavaScript to create a bug with Chrome in iOS.
Open this page in Chrome on an iPhone, and scroll slightly down. The background
should now turn red.
When the background is red, click the link. Notice that the click is not registered.
A second click on the link follows the link.
</p>
<p>
Now, reload the page and scroll down. The background should be red, stop scrolling and
wait until the background turns blue again. Once the background is blue, click the link
and notice that the first click immediately follows the link.
</p>
<p>
Now, reload the page and do not scroll. Click the link and notice that the first click
immediately follows the link.
</p>
<p>
What I have observed is this:
</p>
<ol>
<li>There needs to be an element with position fixed that is hidden</li>
<li>That element is displayed inside a scroll event</li>
<li>There also needs to be a mousedown event handler attached to that element. The event does not have to do anything though</li>
</ol>
<p>
Once all 3 conditions have been met, it requires an additional click to follow the link.
</p>
<p>
Note that I use "opacity: 0" to hide the element, but using "display: none" also seems to work.
</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<a href="https://duckduckgo.com">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
in culpa qui officia deserunt mollit anim id est laborum.
</a>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>Lorem ipsum dolor sit amet</p>
<p>
How to fix this bug? One solution would be to use a
<a href="https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent" target="_blank">PointerEvent</a> instead of the mousedown
event, that fixes this issue. Otherwise, I think that using one for the mousedown event and another event that is hidden. You may
also want to try to not fully hide the element in the "hidden" state, tricking the browser into thinking that the elemen was
always visible. Or perhaps you can try to move the element outside the viewport instead of using opacity.
</p>
<div id="scrollTo"></div>
</div>
<script>
var scrollTimer,
layoutElement = document.getElementById('layout'),
scrollToElement = document.getElementById('scrollTo');
function hideScrollToElement() {
layoutElement.classList.remove('show');
}
function checkScroll() {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(hideScrollToElement, 2000);
if ((window.pageYOffset || document.documentElement.scrollTop) > 10) {
layoutElement.classList.add('show');
} else {
hideScrollToElement();
}
}
scrollToElement.addEventListener('touchstart', function () {});
scrollToElement.addEventListener('mousedown', function () {});
window.addEventListener('scroll', checkScroll);
</script>
</body>
</html> |
Zodra je een element zichtbaar maakt in een scrollevent, waar op dat element ook een mousedown event is gekoppeld, moet je twee keer klikken.
Waarom dit probleem precies speelt weet ik niet, maar ik kan er wel naar raden. Een paar zoekopdrachten over ‘t internet geeft aan dat er vaker browser zijn (geweest) die soortgelijk gedrag vertonen, vaak met de bedoeling om de user experience op mobiele apparaten te verbeteren voor gedrag dat gemaakt is voor desktop apparaten. Op een desktop heb je immers (vaak) een muis, en kun je met een muis hoveren over een link. Op mobiele apparaten kun je dat niet (altijd), daar is het klikken of niet. Wellicht dat Chrome hier slim dacht te zijn het gewenste gedrag van desktop (hover) te reproduceren op een touch device (single touch).
Hoe dan ook, het slechte nieuws is dat het dus een browserbug is, waardoor het oplossen ervan eerder een workaround is dan een echte oplossing.
Gelukkig zijn er tegenwoordig pointer events, daar lijkt dit probleem niet te spelen. Waarschijnlijk omdat dit event nieuwer is en Chrome er dus vanuit gaat dat als je die events gebruikt, het moderne code betreft, en moderne code beter rekening houd met mobiele devices.
Dus ik heb een fix voorgesteld om in deze situatie de pointer events te gebruiken (met een fallback naar mousedown voor browsers die nog geen pointer events ondersteunen).
TLDR: Het zou gefikst moeten zijn
1 Alle browsers op een iPhone gebruiken dezelfde render engine. Het verschil tussen browsers op een iPhone is dus niets anders dan een andere UI. Daarom is cross-browser testen op een iPhone zelden zinvol
2 Het nadeel van dingen digitaal doen is dat je vaak geen idee hebt hoe je zaken moet uitspreken. Dat maakt IRL conversaties soms erg verwarrend
3 Normaal zou een host rewrite van ngrok genoeg moeten zijn om te werken met de lokale omgeving, maar omdat de omgeving automatisch switcht tussen tweakers.net en gathering.tweakers.net op basis van het inkomende domein - en het feit dat we alle URLs absoluut genereren – had dit wat meer voeten in de aarde dan ik had gehoopt. Maar goed, die kennis die ik heb opgedaan is nu voor alle devvers beschikbaar