1313
1414use HeadlessChromium \Communication \Message ;
1515use HeadlessChromium \Page ;
16+ use HeadlessChromium \Utils ;
1617
1718class Mouse
1819{
@@ -162,17 +163,28 @@ public function scrollDown(int $distance)
162163 /**
163164 * Scroll a positive or negative distance using the mouseWheel event type.
164165 *
165- * @param int $distance Distance in pixels
166+ * @param int $distanceY Distance in pixels for the Y axis
167+ * @param int $distanceX (optional) Distance in pixels for the X axis
166168 *
167169 * @throws \HeadlessChromium\Exception\CommunicationException
168170 * @throws \HeadlessChromium\Exception\NoResponseAvailable
171+ * @throws \HeadlessChromium\Exception\OperationTimedOut
169172 *
170173 * @return $this
171174 */
172- private function scroll (int $ distance )
175+ private function scroll (int $ distanceY , int $ distanceX = 0 ): self
173176 {
174177 $ this ->page ->assertNotClosed ();
175178
179+ $ scollableArea = $ this ->page ->getLayoutMetrics ()->getContentSize ();
180+ $ visibleArea = $ this ->page ->getLayoutMetrics ()->getVisualViewport ();
181+
182+ $ distanceX = $ this ->getMaximumDistance ($ distanceX , $ visibleArea ['pageX ' ], $ scollableArea ['height ' ]);
183+ $ distanceY = $ this ->getMaximumDistance ($ distanceY , $ visibleArea ['pageY ' ], $ scollableArea ['width ' ]);
184+
185+ $ targetX = $ visibleArea ['pageX ' ] + $ distanceX ;
186+ $ targetY = $ visibleArea ['pageY ' ] + $ distanceY ;
187+
176188 // make sure the mouse is on the screen
177189 $ this ->move ($ this ->x , $ this ->y );
178190
@@ -181,13 +193,68 @@ private function scroll(int $distance)
181193 'type ' => 'mouseWheel ' ,
182194 'x ' => $ this ->x ,
183195 'y ' => $ this ->y ,
184- 'deltaX ' => 0 ,
185- 'deltaY ' => $ distance ,
196+ 'deltaX ' => $ distanceX ,
197+ 'deltaY ' => $ distanceY ,
186198 ]));
187199
200+ // wait until the scroll is done
201+ Utils::tryWithTimeout (30000 * 1000 , $ this ->waitForScroll ($ targetX , $ targetY ));
202+
188203 // set new position after move
189- $ this ->y += $ distance ;
204+ $ this ->x += $ distanceX ;
205+ $ this ->y += $ distanceY ;
190206
191207 return $ this ;
192208 }
209+
210+ /**
211+ * Get the maximum distance to scroll a page.
212+ *
213+ * @param int $distance Distance to scroll, positive or negative
214+ * @param int $current Current position
215+ * @param int $maximum Maximum posible distance
216+ *
217+ * @return int allowed distance to scroll
218+ */
219+ private function getMaximumDistance (int $ distance , int $ current , int $ maximum ): int
220+ {
221+ $ result = $ current + $ distance ;
222+
223+ if ($ result < 0 ) {
224+ return $ distance + \abs ($ result );
225+ }
226+
227+ if ($ result > $ maximum ) {
228+ return $ maximum - $ current ;
229+ }
230+
231+ return $ distance ;
232+ }
233+
234+ /**
235+ * Wait for the browser to process the scroll command.
236+ *
237+ * Return the number of microseconds to wait before trying again or true in case of success.
238+ *
239+ * @see \HeadlessChromium\Utils::tryWithTimeout
240+ *
241+ * @param int $targetX
242+ * @param int $targetY
243+ *
244+ * @throws \HeadlessChromium\Exception\OperationTimedOut
245+ *
246+ * @return bool|\Generator
247+ */
248+ private function waitForScroll (int $ targetX , int $ targetY )
249+ {
250+ while (true ) {
251+ $ visibleArea = $ this ->page ->getLayoutMetrics ()->getVisualViewport ();
252+
253+ if ($ visibleArea ['pageX ' ] === $ targetX && $ visibleArea ['pageY ' ] === $ targetY ) {
254+ return true ;
255+ }
256+
257+ yield 1000 ;
258+ }
259+ }
193260}
0 commit comments