From 1aa0fced13438e42a5474603650e799e94cb2507 Mon Sep 17 00:00:00 2001 From: Andrey Simonov Date: Mon, 19 May 2025 15:26:36 +1000 Subject: [PATCH 1/2] [#272]: Assert no JS errors. --- src/JavascriptTrait.php | 155 +++++++++++++++++++++++ tests/behat/bootstrap/FeatureContext.php | 2 + tests/behat/features/javascript.feature | 21 +++ 3 files changed, 178 insertions(+) create mode 100644 src/JavascriptTrait.php create mode 100644 tests/behat/features/javascript.feature diff --git a/src/JavascriptTrait.php b/src/JavascriptTrait.php new file mode 100644 index 0000000..27b51bf --- /dev/null +++ b/src/JavascriptTrait.php @@ -0,0 +1,155 @@ +driverIsJsCapable()) { + $this->injectCollector(); + $this->getSession()->getDriver()->executeScript('window.jsErrors = [];'); + } + } + + /** + * Execute JavaScript code on the page. + * + * @When I execute the JavaScript code :code + */ + public function javascriptExecuteCode(string $code): void + { + if (!$this->driverIsJsCapable()) { + throw new \RuntimeException('This step requires a JavaScript-capable driver.'); + } + + // Execute the code directly + try { + $this->getSession()->getDriver()->executeScript($code); + } catch (\Throwable $e) { + // Ignore JavaScript errors + if (stripos($e->getMessage(), 'javascript error') === false) { + throw $e; + } + } + } + + /** + * Assert that at least one JavaScript error occurred. + * + * @Then I should get a JavaScript error + */ + public function javascriptAssertError(): void + { + if (!$this->driverIsJsCapable()) { + throw new \RuntimeException('This step requires a JavaScript-capable driver.'); + } + + $this->injectCollector(); + + $errors = $this->getJavaScriptErrors(); + if (empty($errors)) { + throw new \RuntimeException('No JavaScript errors were found on the page, but at least one was expected.'); + } + } + + /** + * Assert that no JavaScript errors occurred. + * + * @Then there should be no JavaScript errors on the page + */ + public function javascriptAssertNoErrors(): void + { + if (!$this->driverIsJsCapable()) { + throw new \RuntimeException('This step requires a JavaScript-capable driver.'); + } + + $this->injectCollector(); + + $errors = $this->getJavaScriptErrors(); + if (!empty($errors)) { + throw new \RuntimeException("JavaScript errors found on the page: " . json_encode($errors)); + } + } + + + /** + * Inject the JS error collector into the page. + */ + protected function injectCollector(): void + { + $isInstalled = $this->getSession()->getDriver()->executeScript( + 'return typeof window.__behatErrorCollectorInstalled !== "undefined" && window.__behatErrorCollectorInstalled;' + ); + + if ($isInstalled) { + return; + } + + $script = <<<'JS' +(function () { + window.__behatErrorCollectorInstalled = true; + window.jsErrors = window.jsErrors || []; + + window.addEventListener('error', function (event) { + window.jsErrors.push({ + message: event.message || 'Unknown error', + type: 'window.onerror' + }); + }); + + // Intercept console.error + if (!window.__originalConsoleError) { + window.__originalConsoleError = console.error; + console.error = function() { + var message = Array.from(arguments).join(" "); + window.jsErrors.push({ + message: message, + type: "console.error" + }); + window.__originalConsoleError.apply(console, arguments); + }; + } +})(); +JS; + $this->getSession()->getDriver()->executeScript($script); + } + + /** + * Get current JavaScript errors from the page. + * + * @return array> + */ + protected function getJavaScriptErrors(): array + { + try { + $errors = $this->getSession()->getDriver()->executeScript( + 'return (typeof window.jsErrors !== "undefined") ? window.jsErrors : [];' + ); + return is_array($errors) ? $errors : []; + } catch (DriverException $e) { + throw new \RuntimeException('Failed to retrieve JavaScript errors: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Convenience wrapper. + */ + protected function driverIsJsCapable(): bool + { + return $this->getSession()->getDriver() instanceof \Behat\Mink\Driver\Selenium2Driver; + } +} diff --git a/tests/behat/bootstrap/FeatureContext.php b/tests/behat/bootstrap/FeatureContext.php index 879e987..2790696 100644 --- a/tests/behat/bootstrap/FeatureContext.php +++ b/tests/behat/bootstrap/FeatureContext.php @@ -30,6 +30,7 @@ use DrevOps\BehatSteps\ElementTrait; use DrevOps\BehatSteps\FieldTrait; use DrevOps\BehatSteps\FileDownloadTrait; +use DrevOps\BehatSteps\JavascriptTrait; use DrevOps\BehatSteps\KeyboardTrait; use DrevOps\BehatSteps\LinkTrait; use DrevOps\BehatSteps\PathTrait; @@ -70,6 +71,7 @@ class FeatureContext extends DrupalContext { use UserTrait; use WaitTrait; use WatchdogTrait; + use JavascriptTrait; use FeatureContextTrait; diff --git a/tests/behat/features/javascript.feature b/tests/behat/features/javascript.feature new file mode 100644 index 0000000..1adc959 --- /dev/null +++ b/tests/behat/features/javascript.feature @@ -0,0 +1,21 @@ +Feature: Check that JavascriptTrait works + As a Behat Steps library developer + I want to provide tools to track and verify JavaScript errors + So that users can ensure their pages are error-free + + @javascript + Scenario: Assert no JavaScript errors when none exist + When I go to "/" + Then there should be no JavaScript errors on the page + + @javascript + Scenario: Assert JavaScript error is detected when one exists + When I go to "/" + And I execute the JavaScript code "throw new Error('Test JS error');" + Then I should get a JavaScript error + + @javascript + Scenario: Assert no JavaScript errors fails when errors exist + When I go to "/" + And I execute the JavaScript code "console.error('Test JS error');" + Then I should get a JavaScript error From 5696e81d3ec1f4b7f3f2bb40808c97e86e9425cd Mon Sep 17 00:00:00 2001 From: Andrey Simonov Date: Tue, 20 May 2025 11:17:36 +1000 Subject: [PATCH 2/2] [#272]: Updated relative.html --- src/JavascriptTrait.php | 247 ++++++++++++++---------- tests/behat/features/javascript.feature | 17 +- tests/behat/fixtures/relative.html | 5 + 3 files changed, 153 insertions(+), 116 deletions(-) diff --git a/src/JavascriptTrait.php b/src/JavascriptTrait.php index 27b51bf..cb46b5e 100644 --- a/src/JavascriptTrait.php +++ b/src/JavascriptTrait.php @@ -4,152 +4,189 @@ namespace DrevOps\BehatSteps; +use Behat\Behat\Hook\Scope\AfterScenarioScope; +use Behat\Behat\Hook\Scope\AfterStepScope; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Behat\Hook\Scope\BeforeStepScope; use Behat\Mink\Exception\DriverException; -/** - * Provides Behat step definitions for tracking and verifying JavaScript errors - * in web pages using a JavaScript-capable driver. - */ trait JavascriptTrait { - /** - * Empty the in-page error buffer at the beginning of every scenario. - * - * @BeforeScenario @javascript - */ - public function javascriptResetErrors(): void + private array $jsErrorRegistry = []; + + private array $monitoredStepPatterns = [ + '/I visit /i', + '/I go to /i', + '/I open /i', + '/I click /i', + '/I press /i', + ]; + + public function addJsMonitorStepPattern(string $regex): void { - if ($this->driverIsJsCapable()) { - $this->injectCollector(); - $this->getSession()->getDriver()->executeScript('window.jsErrors = [];'); - } + $this->monitoredStepPatterns[] = $regex; + } + + /** @BeforeScenario @javascript */ + public function jsCollectorBeforeScenario(BeforeScenarioScope $scope): void + { + $this->jsErrorRegistry = []; } - /** - * Execute JavaScript code on the page. - * - * @When I execute the JavaScript code :code - */ - public function javascriptExecuteCode(string $code): void + /** @BeforeStep @javascript */ + public function jsCollectorBeforeStep(BeforeStepScope $scope): void { + if (!$this->stepNeedsMonitoring($scope->getStep()->getText())) { + return; + } if (!$this->driverIsJsCapable()) { - throw new \RuntimeException('This step requires a JavaScript-capable driver.'); + return; } - // Execute the code directly - try { - $this->getSession()->getDriver()->executeScript($code); - } catch (\Throwable $e) { - // Ignore JavaScript errors - if (stripos($e->getMessage(), 'javascript error') === false) { - throw $e; - } - } + $this->injectJsCollector(); + $this->safeExecute('window.__behatJsErrors = [];'); } - /** - * Assert that at least one JavaScript error occurred. - * - * @Then I should get a JavaScript error - */ - public function javascriptAssertError(): void + /** @AfterStep @javascript */ + public function jsCollectorAfterStep(AfterStepScope $scope): void { + if (!$this->stepNeedsMonitoring($scope->getStep()->getText())) { + return; + } if (!$this->driverIsJsCapable()) { - throw new \RuntimeException('This step requires a JavaScript-capable driver.'); + return; } - $this->injectCollector(); + $this->injectJsCollector(); + $this->harvestErrorsIntoRegistry(); + } - $errors = $this->getJavaScriptErrors(); - if (empty($errors)) { - throw new \RuntimeException('No JavaScript errors were found on the page, but at least one was expected.'); + /** @AfterScenario @no-js-errors @javascript */ + public function jsCollectorAssertScenarioClean(AfterScenarioScope $scope): void + { + $this->harvestErrorsIntoRegistry(); + if ($this->flattenRegistry() !== []) { + throw new \RuntimeException( + "JavaScript errors detected during scenario:\n" . + json_encode($this->jsErrorRegistry, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); } } - /** - * Assert that no JavaScript errors occurred. - * - * @Then there should be no JavaScript errors on the page - */ - public function javascriptAssertNoErrors(): void + /** @Then /^no JavaScript errors should have occurred on this page$/ */ + public function assertNoJsErrorsOnCurrentPage(): void { if (!$this->driverIsJsCapable()) { - throw new \RuntimeException('This step requires a JavaScript-capable driver.'); + return; } - $this->injectCollector(); + $this->harvestErrorsIntoRegistry(); + $path = $this->currentPath(); + $errors = $this->jsErrorRegistry[$path] ?? []; - $errors = $this->getJavaScriptErrors(); - if (!empty($errors)) { - throw new \RuntimeException("JavaScript errors found on the page: " . json_encode($errors)); + if ($errors !== []) { + throw new \RuntimeException( + "JavaScript errors on page $path:\n" . + json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); } } - - /** - * Inject the JS error collector into the page. - */ - protected function injectCollector(): void + private function injectJsCollector(): void { - $isInstalled = $this->getSession()->getDriver()->executeScript( - 'return typeof window.__behatErrorCollectorInstalled !== "undefined" && window.__behatErrorCollectorInstalled;' - ); + $js = <<<'JS' + (function () { + if (window.__behatJsCollectorInstalled) { return; } + window.__behatJsCollectorInstalled = true; + window.__behatJsErrors = window.__behatJsErrors || []; + + function push(msg) { + try { window.__behatJsErrors.push(String(msg)); } catch (e) {} + } + + var origConsoleError = console.error; + console.error = function () { + push([].slice.call(arguments).join(' ')); + return origConsoleError.apply(console, arguments); + }; + + var origOnError = window.onerror; + window.onerror = function (msg, src, line, col) { + push(msg + ' @ ' + src + ':' + line + ':' + col); + if (origOnError) { return origOnError.apply(this, arguments); } + }; + + window.addEventListener('error', function (ev) { + if (ev.target && (ev.target.src || ev.target.href)) { + push('Resource error: ' + (ev.target.src || ev.target.href)); + } + }, true); + + window.addEventListener('unhandledrejection', function (ev) { + push('Unhandled rejection: ' + (ev.reason && ev.reason.message + ? ev.reason.message + : ev.reason)); + }); + }()); + JS; + + $this->safeExecute($js); + } - if ($isInstalled) { + private function harvestErrorsIntoRegistry(): void + { + try { + $errors = $this->getSession() + ->evaluateScript('return window.__behatJsErrors || [];'); + } catch (DriverException $e) { return; } - $script = <<<'JS' -(function () { - window.__behatErrorCollectorInstalled = true; - window.jsErrors = window.jsErrors || []; - - window.addEventListener('error', function (event) { - window.jsErrors.push({ - message: event.message || 'Unknown error', - type: 'window.onerror' - }); - }); - - // Intercept console.error - if (!window.__originalConsoleError) { - window.__originalConsoleError = console.error; - console.error = function() { - var message = Array.from(arguments).join(" "); - window.jsErrors.push({ - message: message, - type: "console.error" - }); - window.__originalConsoleError.apply(console, arguments); - }; + if (!is_array($errors) || $errors === []) { + return; } -})(); -JS; - $this->getSession()->getDriver()->executeScript($script); + + $path = $this->currentPath(); + $this->jsErrorRegistry[$path] = array_merge( + $this->jsErrorRegistry[$path] ?? [], + $errors + ); + + $this->safeExecute('window.__behatJsErrors = [];'); } - /** - * Get current JavaScript errors from the page. - * - * @return array> - */ - protected function getJavaScriptErrors(): array + private function stepNeedsMonitoring(string $text): bool { - try { - $errors = $this->getSession()->getDriver()->executeScript( - 'return (typeof window.jsErrors !== "undefined") ? window.jsErrors : [];' - ); - return is_array($errors) ? $errors : []; - } catch (DriverException $e) { - throw new \RuntimeException('Failed to retrieve JavaScript errors: ' . $e->getMessage(), 0, $e); + foreach ($this->monitoredStepPatterns as $rx) { + if (preg_match($rx, $text)) { + return true; + } } + return false; } - /** - * Convenience wrapper. - */ - protected function driverIsJsCapable(): bool + private function flattenRegistry(): array { - return $this->getSession()->getDriver() instanceof \Behat\Mink\Driver\Selenium2Driver; + return array_merge(...array_values($this->jsErrorRegistry ?: [[]])); + } + + private function currentPath(): string + { + $url = $this->getSession()->getCurrentUrl(); + return parse_url($url, PHP_URL_PATH) ?: $url; + } + + private function driverIsJsCapable(): bool + { + return method_exists($this, 'getSession') + && $this->getSession()->getDriver()->supportsJavascript(); + } + + private function safeExecute(string $script): void + { + try { + $this->getSession()->executeScript($script); + } catch (DriverException $e) { + } } } diff --git a/tests/behat/features/javascript.feature b/tests/behat/features/javascript.feature index 1adc959..4dc6e1c 100644 --- a/tests/behat/features/javascript.feature +++ b/tests/behat/features/javascript.feature @@ -3,19 +3,14 @@ Feature: Check that JavascriptTrait works I want to provide tools to track and verify JavaScript errors So that users can ensure their pages are error-free - @javascript + @javascript @no-js-errors Scenario: Assert no JavaScript errors when none exist When I go to "/" - Then there should be no JavaScript errors on the page - @javascript + + @javascript @no-js-errors Scenario: Assert JavaScript error is detected when one exists - When I go to "/" - And I execute the JavaScript code "throw new Error('Test JS error');" - Then I should get a JavaScript error + When I visit "/sites/default/files/relative.html" + And the element "#js-error-trigger" should be displayed + And I click on the element "#js-error-trigger" - @javascript - Scenario: Assert no JavaScript errors fails when errors exist - When I go to "/" - And I execute the JavaScript code "console.error('Test JS error');" - Then I should get a JavaScript error diff --git a/tests/behat/fixtures/relative.html b/tests/behat/fixtures/relative.html index 092223c..aca8215 100644 --- a/tests/behat/fixtures/relative.html +++ b/tests/behat/fixtures/relative.html @@ -440,6 +440,11 @@ Show off-canvas overlay + + +