From 2ba53d10205272dd8682309777b26fba5756e0dd Mon Sep 17 00:00:00 2001 From: otsch Date: Sun, 12 Jan 2025 16:46:11 +0100 Subject: [PATCH 1/3] Fix scrolling timeout issue This change addresses a potential issue where the maximum scrolling distance may change between calculating it, sending the scroll message, and verifying the new position. If verifying the position times out, the code now checks if the scrolling distance has changed. If it has, scrolling is retried once. Additionally, the maximum wait time for verifying the new position has been reduced from 30 seconds to 3 seconds. --- src/Input/Mouse.php | 77 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/src/Input/Mouse.php b/src/Input/Mouse.php index 5aae49f..b424123 100644 --- a/src/Input/Mouse.php +++ b/src/Input/Mouse.php @@ -141,6 +141,7 @@ public function click(?array $options = null) * * @throws \HeadlessChromium\Exception\CommunicationException * @throws \HeadlessChromium\Exception\NoResponseAvailable + * @throws \HeadlessChromium\Exception\OperationTimedOut * * @return $this */ @@ -156,6 +157,7 @@ public function scrollUp(int $distance) * * @throws \HeadlessChromium\Exception\CommunicationException * @throws \HeadlessChromium\Exception\NoResponseAvailable + * @throws \HeadlessChromium\Exception\OperationTimedOut * * @return $this */ @@ -180,6 +182,52 @@ private function scroll(int $distanceY, int $distanceX = 0): self { $this->page->assertNotClosed(); + // make sure the mouse is on the screen + $this->move($this->x, $this->y); + + [$distances, $targets] = $this->getScrollDistancesAndTargets($distanceY, $distanceX); + + // scroll + $this->sendScrollMessage($distances); + + try { + // wait until the scroll is done + Utils::tryWithTimeout(3_000_000, $this->waitForScroll($targets['x'], $targets['y'])); + } catch (\HeadlessChromium\Exception\OperationTimedOut $exception) { + // Maybe the possible max scroll distances changed in the meantime. + $prevDistances = $distances; + + [$distances, $targets] = $this->getScrollDistancesAndTargets($distanceY, $distanceX); + + if ($prevDistances === $distances) { + throw $exception; + } + + if ($distanceY !== 0 || $distanceX !== 0) { // Try with the new values. + $this->sendScrollMessage($distances); + + // wait until the scroll is done + Utils::tryWithTimeout(3_000_000, $this->waitForScroll($targets['x'], $targets['y'])); + } + } + + // set new position after move + $this->x += $distances['x']; + $this->y += $distances['y']; + + return $this; + } + + /** + * @throws \HeadlessChromium\Exception\OperationTimedOut + * @throws \HeadlessChromium\Exception\CommunicationException + * @throws \HeadlessChromium\Exception\CommunicationException\ResponseHasError + * @throws \HeadlessChromium\Exception\NoResponseAvailable + * + * @return array{ distances: array{ x: int, y: int }, targets: array{ x: int, y: int } } + */ + private function getScrollDistancesAndTargets(int $distanceY, int $distanceX = 0): array + { $scrollableArea = $this->page->getLayoutMetrics()->getCssContentSize(); $visibleArea = $this->page->getLayoutMetrics()->getCssVisualViewport(); @@ -192,26 +240,27 @@ private function scroll(int $distanceY, int $distanceX = 0): self $targetX = $visibleArea['pageX'] + $distanceX; $targetY = $visibleArea['pageY'] + $distanceY; - // make sure the mouse is on the screen - $this->move($this->x, $this->y); + return [ + ['x' => $distanceX, 'y' => $distanceY], + ['x' => $targetX, 'y' => $targetY], + ]; + } - // scroll + /** + * @param array{ x: int, y: int } $distances + * + * @throws \HeadlessChromium\Exception\CommunicationException + * @throws \HeadlessChromium\Exception\NoResponseAvailable + */ + private function sendScrollMessage(array $distances): void + { $this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [ 'type' => 'mouseWheel', 'x' => $this->x, 'y' => $this->y, - 'deltaX' => $distanceX, - 'deltaY' => $distanceY, + 'deltaX' => $distances['x'], + 'deltaY' => $distances['y'], ])); - - // wait until the scroll is done - Utils::tryWithTimeout(30000 * 1000, $this->waitForScroll($targetX, $targetY)); - - // set new position after move - $this->x += $distanceX; - $this->y += $distanceY; - - return $this; } /** From 96d797d64d3d4bb2fe3a5066f5b2d0df722f5449 Mon Sep 17 00:00:00 2001 From: otsch Date: Sun, 12 Jan 2025 17:15:46 +0100 Subject: [PATCH 2/3] Fix phpstan issue --- src/Input/Mouse.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Input/Mouse.php b/src/Input/Mouse.php index b424123..e8eb259 100644 --- a/src/Input/Mouse.php +++ b/src/Input/Mouse.php @@ -224,7 +224,7 @@ private function scroll(int $distanceY, int $distanceX = 0): self * @throws \HeadlessChromium\Exception\CommunicationException\ResponseHasError * @throws \HeadlessChromium\Exception\NoResponseAvailable * - * @return array{ distances: array{ x: int, y: int }, targets: array{ x: int, y: int } } + * @return array{ array{ x: int, y: int }, array{ x: int, y: int } } */ private function getScrollDistancesAndTargets(int $distanceY, int $distanceX = 0): array { @@ -242,7 +242,7 @@ private function getScrollDistancesAndTargets(int $distanceY, int $distanceX = 0 return [ ['x' => $distanceX, 'y' => $distanceY], - ['x' => $targetX, 'y' => $targetY], + ['x' => (int) $targetX, 'y' => (int) $targetY], ]; } From e04015b803295ab2d8ade054f0686d9d25a12797 Mon Sep 17 00:00:00 2001 From: otsch Date: Sun, 12 Jan 2025 19:38:51 +0100 Subject: [PATCH 3/3] Fix code style issue --- src/Input/Mouse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Input/Mouse.php b/src/Input/Mouse.php index e8eb259..a7f50ca 100644 --- a/src/Input/Mouse.php +++ b/src/Input/Mouse.php @@ -203,7 +203,7 @@ private function scroll(int $distanceY, int $distanceX = 0): self throw $exception; } - if ($distanceY !== 0 || $distanceX !== 0) { // Try with the new values. + if (0 !== $distanceY || 0 !== $distanceX) { // Try with the new values. $this->sendScrollMessage($distances); // wait until the scroll is done