From c19fe291aab8a15533b7edfe02dfe17382c3e631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 14 Sep 2025 22:42:52 +0200 Subject: [PATCH] Fix script evaluation --- src/Locator/Locator.php | 29 ++++++++- src/Page/Page.php | 29 ++++++++- .../Locator/LocatorEvaluateNormalizerTest.php | 56 ++++++++++++++++++ .../Unit/Page/PageEvaluateNormalizerTest.php | 59 +++++++++++++++++++ 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Locator/LocatorEvaluateNormalizerTest.php create mode 100644 tests/Unit/Page/PageEvaluateNormalizerTest.php diff --git a/src/Locator/Locator.php b/src/Locator/Locator.php index 015532e..1a77666 100644 --- a/src/Locator/Locator.php +++ b/src/Locator/Locator.php @@ -407,7 +407,8 @@ public function nth(int $index): self public function evaluate(string $expression, mixed $arg = null): mixed { - $response = $this->sendCommand('locator.evaluate', ['expression' => $expression, 'arg' => $arg]); + $normalized = self::normalizeForLocator($expression); + $response = $this->sendCommand('locator.evaluate', ['expression' => $normalized, 'arg' => $arg]); return $response['value'] ?? null; } @@ -447,6 +448,32 @@ private function sendCommand(string $action, array $params = []): array return $response; } + private static function normalizeForLocator(string $expression): string + { + $trimmed = ltrim($expression); + + if (self::isFunctionLike($trimmed)) { + return $expression; + } + + if (self::startsWithReturn($trimmed)) { + return '(el, arg) => { '.$trimmed.' }'; + } + + return $expression; + } + + private static function isFunctionLike(string $s): bool + { + // Detect common JS function patterns: function, async function, arrow functions + return (bool) preg_match('/^((async\s+)?function\b|\([^)]*\)\s*=>|[A-Za-z_$][A-Za-z0-9_$]*\s*=>|async\s*\([^)]*\)\s*=>)/', $s); + } + + private static function startsWithReturn(string $s): bool + { + return (bool) preg_match('/^return\b/', $s); + } + /** * @param array $options */ diff --git a/src/Page/Page.php b/src/Page/Page.php index 4f8940c..874fbef 100644 --- a/src/Page/Page.php +++ b/src/Page/Page.php @@ -259,7 +259,8 @@ public function content(): ?string public function evaluate(string $expression, mixed $arg = null): mixed { - $response = $this->sendCommand('evaluate', ['expression' => $expression, 'arg' => $arg]); + $normalized = self::normalizeForPage($expression); + $response = $this->sendCommand('evaluate', ['expression' => $normalized, 'arg' => $arg]); return $response['result'] ?? null; } @@ -751,6 +752,32 @@ private function createConsoleMessage(array $params): ConsoleMessage return new ConsoleMessage($params); } + private static function normalizeForPage(string $expression): string + { + $trimmed = ltrim($expression); + + if (self::isFunctionLike($trimmed)) { + return $expression; + } + + if (self::startsWithReturn($trimmed)) { + return '(arg) => { '.$trimmed.' }'; + } + + return $expression; + } + + private static function isFunctionLike(string $s): bool + { + // Detect common JS function patterns: function, async function, arrow functions + return (bool) preg_match('/^((async\s+)?function\b|\([^)]*\)\s*=>|[A-Za-z_$][A-Za-z0-9_$]*\s*=>|async\s*\([^)]*\)\s*=>)/', $s); + } + + private static function startsWithReturn(string $s): bool + { + return (bool) preg_match('/^return\b/', $s); + } + /** * Helper method to validate transport data for Request creation. * diff --git a/tests/Unit/Locator/LocatorEvaluateNormalizerTest.php b/tests/Unit/Locator/LocatorEvaluateNormalizerTest.php new file mode 100644 index 0000000..9e2e6d0 --- /dev/null +++ b/tests/Unit/Locator/LocatorEvaluateNormalizerTest.php @@ -0,0 +1,56 @@ +createMock(TransportInterface::class); + + $transport + ->expects($this->once()) + ->method('send') + ->with($this->callback(function ($payload) { + return 'locator.evaluate' === $payload['action'] + && '(el, arg) => { return el.textContent; }' === $payload['expression']; + })) + ->willReturn(['value' => 'hello']); + + $locator = new Locator($transport, 'page1', '.title'); + $result = $locator->evaluate('return el.textContent;'); + $this->assertSame('hello', $result); + } + + public function testLeavesPlainExpressionUntouched(): void + { + $transport = $this->createMock(TransportInterface::class); + + $transport + ->expects($this->once()) + ->method('send') + ->with($this->callback(function ($payload) { + return 'locator.evaluate' === $payload['action'] + && 'element.textContent' === $payload['expression']; + })) + ->willReturn(['value' => 'ok']); + + $locator = new Locator($transport, 'page1', '.title'); + $result = $locator->evaluate('element.textContent'); + $this->assertSame('ok', $result); + } +} diff --git a/tests/Unit/Page/PageEvaluateNormalizerTest.php b/tests/Unit/Page/PageEvaluateNormalizerTest.php new file mode 100644 index 0000000..81e4de3 --- /dev/null +++ b/tests/Unit/Page/PageEvaluateNormalizerTest.php @@ -0,0 +1,59 @@ +createMock(TransportInterface::class); + $context = $this->createMock(BrowserContextInterface::class); + + $transport + ->expects($this->once()) + ->method('send') + ->with($this->callback(function ($payload) { + return 'page.evaluate' === $payload['action'] + && '(arg) => { return 42; }' === $payload['expression']; + })) + ->willReturn(['result' => 42]); + + $page = new Page($transport, $context, 'p1'); + $result = $page->evaluate('return 42;'); + $this->assertSame(42, $result); + } + + public function testLeavesPlainExpressionUntouched(): void + { + $transport = $this->createMock(TransportInterface::class); + $context = $this->createMock(BrowserContextInterface::class); + + $transport + ->expects($this->once()) + ->method('send') + ->with($this->callback(function ($payload) { + return 'page.evaluate' === $payload['action'] + && 'document.title' === $payload['expression']; + })) + ->willReturn(['result' => 'Hello']); + + $page = new Page($transport, $context, 'p1'); + $result = $page->evaluate('document.title'); + $this->assertSame('Hello', $result); + } +}