├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── features ├── bootstrap │ └── FeatureContext.php └── mink_debug.feature ├── src ├── Listener │ └── FailedStepListener.php └── ServiceContainer │ └── MinkDebugExtension.php └── test-application ├── behat.yml.dist ├── features └── test.feature └── logs └── .gitkeep /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened, synchronize, edited, reopened] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | continue-on-error: false 12 | name: "PHP ${{ matrix.php }}" 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | php: 18 | - '7.4' 19 | - '8.0' 20 | - '8.1' 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Setup PHP 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | coverage: none 30 | ini-values: "memory_limit=-1" 31 | php-version: ${{ matrix.php }} 32 | tools: composer:v2 33 | 34 | - name: Run Chrome Headless 35 | run: google-chrome-stable --enable-automation --disable-background-networking --no-default-browser-check --no-first-run --disable-popup-blocking --disable-default-apps --allow-insecure-localhost --disable-translate --disable-extensions --no-sandbox --enable-features=Metal --headless --remote-debugging-port=9222 --window-size=2880,1800 --proxy-server='direct://' --proxy-bypass-list='*' http://127.0.0.1 > /dev/null 2>&1 & 36 | 37 | - name: Get Composer cache directory 38 | id: composer-cache 39 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 40 | 41 | - name: Cache Composer 42 | uses: actions/cache@v2 43 | with: 44 | path: ${{ steps.composer-cache.outputs.dir }} 45 | key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json **/composer.lock') }} 46 | restore-keys: | 47 | ${{ runner.os }}-php-${{ matrix.php }}-composer- 48 | 49 | - name: Install PHP dependencies 50 | run: composer install --no-interaction 51 | 52 | - name: Validate composer.json 53 | run: composer validate --ansi --strict 54 | 55 | - name: Run Behat 56 | run: vendor/bin/behat --colors --strict --no-interaction -vvv -f progress 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | 4 | /behat.yml 5 | 6 | /test-application/logs/* 7 | !/test-application/logs/.gitkeep 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### v2.0.1 4 | 5 | - [#35](https://github.com/FriendsOfBehat/MinkDebugExtension/issues/35) Ignore StreamReadException as well ([@pamil](https://github.com/pamil)) 6 | 7 | ### v2.0.0 8 | 9 | * Added support for PHP 8.0 10 | * Allowed taking screenshots with more drivers than just Selenium2Driver 11 | * Changed log files extension from `.log` to `.html` 12 | * Removed supplementary `upload-textfiles`, `upload-screenshots`, `wait-for-port` binaries 13 | * Renamed extension from `Lakion\Behat\MinkDebugExtension` to `FriendsOfBehat\MinkDebugExtension` 14 | 15 | ### v1.0.0 16 | 17 | * Initial release. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2020 Lakion 2 | 2020-2021 Kamil Kokot 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MinkDebugExtension 2 | ================== 3 | 4 | **MinkDebugExtension** is a Behat extension made for debugging and logging Mink related data after every failed step. 5 | It is especially useful while running tests on continuous integration server like Travis. 6 | While using appropriate driver, you can also save screenshots just after the failure. 7 | 8 | Installation 9 | ------------ 10 | 11 | Assuming you already have Composer: 12 | 13 | ```bash 14 | composer require friends-of-behat/mink-debug-extension 15 | ``` 16 | 17 | Then you only need to configure your Behat profile: 18 | 19 | ```yml 20 | default: 21 | extensions: 22 | FriendsOfBehat\MinkDebugExtension: 23 | directory: directory-where-to-save-logs 24 | ``` 25 | 26 | Configuration reference 27 | ----------------------- 28 | 29 | Under `FriendsOfBehat\MinkDebugExtension` there are three options to be configured: 30 | 31 | - `directory` (required to enable extension) - contains path to directory that will contain generated logs. Use the variable `%paths.base%` to refer to the directory where your `behat.yml` is 32 | - `screenshot` (default `false`) - whether to save screenshots if using supporting driver 33 | - `clean_start` (default `true`) - whether to clean your existing logs on each Behat execution 34 | 35 | Testing 36 | ------- 37 | 38 | In order to test the extensions run: 39 | 40 | ```bash 41 | composer install 42 | bin/behat --strict 43 | ``` 44 | 45 | Authors 46 | ------- 47 | 48 | MinkDebugExtension was originally created by [Kamil Kokot](https://kamilkokot.com). 49 | See the list of [contributors](https://github.com/FriendsOfBehat/MinkDebugExtension/contributors). 50 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # UPGRADE 2 | 3 | ## FROM `1.x` TO `2.x` 4 | 5 | - Change required package from `lakion/mink-debug-extension` to `friends-of-behat/mink-debug-extension` in your `composer.json` 6 | - Change extension name from `Lakion\Behat\MinkDebugExtension` to `FriendsOfBehat\MinkDebugExtension` in your `behat.yml` 7 | - Make sure you're not using binaries provided by `1.x` version of this library 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friends-of-behat/mink-debug-extension", 3 | "type": "behat-extension", 4 | "description": "Debug extension for Behat", 5 | "keywords": [ 6 | "debug", 7 | "behat", 8 | "mink", 9 | "logging" 10 | ], 11 | "homepage": "https://github.com/FriendsOfBehat/MinkDebugExtension", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Kamil Kokot", 16 | "email": "kamil@kokot.me", 17 | "homepage": "https://kamilkokot.com" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=7.4", 22 | "behat/behat": "^3.5", 23 | "behat/mink-extension": "^2.3" 24 | }, 25 | "require-dev": { 26 | "behat/mink-goutte-driver": "^1.2", 27 | "behat/mink-selenium2-driver": "^1.4", 28 | "dmore/behat-chrome-extension": "^1.3", 29 | "dmore/chrome-mink-driver": "^2.7", 30 | "symfony/process": "^4.4 || ^5.2" 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "2.1-dev" 35 | } 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "FriendsOfBehat\\MinkDebugExtension\\": "src/" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | */ 16 | private array $configuration = ['%clean_start%' => 'true']; 17 | 18 | /** @var string */ 19 | private string $testApplicationDir; 20 | 21 | /** 22 | * @BeforeScenario 23 | */ 24 | public function prepareProcess(): void 25 | { 26 | $phpFinder = new PhpExecutableFinder(); 27 | if (false === $php = $phpFinder->find()) { 28 | throw new \RuntimeException('Unable to find the PHP executable.'); 29 | } 30 | 31 | $this->phpBin = $php; 32 | $this->testApplicationDir = __DIR__ . '/../../test-application'; 33 | } 34 | 35 | /** 36 | * @Given there is following Behat extension configuration: 37 | */ 38 | public function thereIsBehatExtensionConfiguration(TableNode $table): void 39 | { 40 | foreach ($table->getRowsHash() as $key => $value) { 41 | $this->configuration['%' . $key . '%'] = $value; 42 | } 43 | } 44 | 45 | /** 46 | * @Given /configuration option "([^"]+?)" is set to "([^"]+?)"/ 47 | */ 48 | public function configurationOptionSet(string $key, string $value): void 49 | { 50 | $this->configuration['%' . $key . '%'] = $value; 51 | } 52 | 53 | /** 54 | * @When /I run Behat with failing scenarios(?: using (.+?) profile)?/ 55 | */ 56 | public function iRunBehat(?string $profile = null): void 57 | { 58 | $this->createBehatConfigurationFile(); 59 | 60 | $this->doRunBehat($this->getExtraConfiguration($profile)); 61 | 62 | $this->deleteBehatConfigurationFile(); 63 | } 64 | 65 | /** 66 | * @Then there should be text log generated 67 | */ 68 | public function thereShouldBeTextLogGenerated(): void 69 | { 70 | $logPattern = $this->testApplicationDir . '/' . $this->configuration['%directory%'] . '/*.html'; 71 | 72 | $logsAmount = count(glob($logPattern)); 73 | if ($logsAmount !== 1) { 74 | throw new \RuntimeException(sprintf('Expected 1 log file, found %d.', $logsAmount)); 75 | } 76 | } 77 | 78 | /** 79 | * @Then a screenshot should be made 80 | */ 81 | public function screenshotShouldBeMade(): void 82 | { 83 | $screenshotPattern = $this->testApplicationDir . '/' . $this->configuration['%directory%'] . '/*.png'; 84 | 85 | $screenshotsAmount = count(glob($screenshotPattern)); 86 | if ($screenshotsAmount !== 1) { 87 | throw new \RuntimeException(sprintf('Expected 1 screenshot, found %d.', $screenshotsAmount)); 88 | } 89 | } 90 | 91 | /** 92 | * @Then a screenshot should not be made 93 | */ 94 | public function screenshotShouldNotBeMade(): void 95 | { 96 | $screenshotPattern = $this->testApplicationDir . '/' . $this->configuration['%directory%'] . '/*.png'; 97 | 98 | $screenshotsAmount = count(glob($screenshotPattern)); 99 | if ($screenshotsAmount !== 0) { 100 | throw new \RuntimeException(sprintf('Expected no screenshots, found %d.', $screenshotsAmount)); 101 | } 102 | } 103 | 104 | private function createBehatConfigurationFile(): void 105 | { 106 | $behatConfiguration = strtr( 107 | file_get_contents($this->testApplicationDir . '/behat.yml.dist'), 108 | $this->configuration 109 | ); 110 | 111 | file_put_contents($this->testApplicationDir . '/behat.yml', $behatConfiguration); 112 | } 113 | 114 | private function getExtraConfiguration(?string $profile): array 115 | { 116 | if (null !== $profile) { 117 | return ['--profile=' . $profile]; 118 | } 119 | 120 | return []; 121 | } 122 | 123 | private function doRunBehat(array $extraConfiguration): void 124 | { 125 | $arguments = array_merge( 126 | [$this->phpBin, BEHAT_BIN_PATH, '--strict', '-vvv', '--no-interaction', '--lang=en'], 127 | $extraConfiguration 128 | ); 129 | 130 | $process = new Process($arguments, $this->testApplicationDir); 131 | $process->start(); 132 | $process->wait(); 133 | 134 | printf("stdOut:\n %s\nstdErr:\n%s\n", $process->getOutput(), $process->getErrorOutput()); 135 | } 136 | 137 | private function deleteBehatConfigurationFile(): void 138 | { 139 | if (file_exists($behatFile = $this->testApplicationDir . '/behat.yml')) { 140 | unlink($behatFile); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /features/mink_debug.feature: -------------------------------------------------------------------------------- 1 | Feature: Logging debug data 2 | In order to debug my Behat suites with ease 3 | As a developer 4 | I want to be able to access logs 5 | 6 | Background: 7 | Given there is following Behat extension configuration: 8 | | directory | logs | 9 | | screenshot | true | 10 | 11 | Scenario: 12 | When I run Behat with failing scenarios 13 | Then there should be text log generated 14 | 15 | Scenario: 16 | When I run Behat with failing scenarios using javascript profile 17 | Then there should be text log generated 18 | And a screenshot should be made 19 | 20 | Scenario: 21 | Given configuration option "screenshot" is set to "false" 22 | When I run Behat with failing scenarios using javascript profile 23 | Then there should be text log generated 24 | And a screenshot should not be made 25 | -------------------------------------------------------------------------------- /src/Listener/FailedStepListener.php: -------------------------------------------------------------------------------- 1 | mink = $mink; 45 | $this->logDirectory = $logDirectory; 46 | $this->screenshot = $screenshot; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public static function getSubscribedEvents(): array 53 | { 54 | return [ 55 | StepTested::AFTER => ['logFailedStepInformations', -10], 56 | ]; 57 | } 58 | 59 | public function logFailedStepInformations(AfterStepTested $event): void 60 | { 61 | $testResult = $event->getTestResult(); 62 | 63 | if (!$testResult instanceof TestResult || TestResult::FAILED !== $testResult->getResultCode()) { 64 | return; 65 | } 66 | 67 | if (!$this->hasEligibleMinkSession()) { 68 | return; 69 | } 70 | 71 | $this->currentDateAsString = date('YmdHis'); 72 | 73 | $this->logPageContent(); 74 | 75 | if ($this->screenshot) { 76 | $this->logScreenshot(); 77 | } 78 | } 79 | 80 | private function logPageContent(): void 81 | { 82 | $session = $this->getSession(); 83 | 84 | $log = sprintf('Current page: %d %s', $this->getStatusCode($session), $this->getCurrentUrl($session)) . "\n"; 85 | $log .= $this->getResponseHeadersLogMessage($session); 86 | $log .= $this->getResponseContentLogMessage($session); 87 | 88 | $this->saveLog($log, 'html'); 89 | } 90 | 91 | private function logScreenshot(): void 92 | { 93 | $session = $this->getSession(); 94 | 95 | try { 96 | $this->saveLog($session->getScreenshot(), 'png'); 97 | } catch (UnsupportedDriverActionException | WebDriverException $exception) {} 98 | } 99 | 100 | private function saveLog(string $content, string $type): void 101 | { 102 | $path = sprintf("%s/behat-%s.%s", $this->logDirectory, $this->currentDateAsString, $type); 103 | 104 | if (file_put_contents($path, $content) === false) { 105 | throw new \RuntimeException(sprintf('Failed while trying to write log in "%s".', $path)); 106 | } 107 | } 108 | 109 | private function getSession(?string $name = null): Session 110 | { 111 | return $this->mink->getSession($name); 112 | } 113 | 114 | private function hasEligibleMinkSession(?string $name = null): bool 115 | { 116 | $name = $name ?: $this->mink->getDefaultSessionName(); 117 | 118 | return $this->mink->hasSession($name) && $this->mink->isSessionStarted($name); 119 | } 120 | 121 | private function getStatusCode(Session $session): ?int 122 | { 123 | try { 124 | return $session->getStatusCode(); 125 | } catch (MinkException | WebDriverException | StreamReadException $exception) { 126 | return null; 127 | } 128 | } 129 | 130 | private function getCurrentUrl(Session $session): ?string 131 | { 132 | try { 133 | return $session->getCurrentUrl(); 134 | } catch (MinkException | WebDriverException | StreamReadException $exception) { 135 | return null; 136 | } 137 | } 138 | 139 | private function getResponseHeadersLogMessage(Session $session): ?string 140 | { 141 | try { 142 | return 'Response headers:' . "\n" . print_r($session->getResponseHeaders(), true) . "\n"; 143 | } catch (MinkException | WebDriverException | StreamReadException $exception) { 144 | return null; 145 | } 146 | } 147 | 148 | private function getResponseContentLogMessage(Session $session): ?string 149 | { 150 | try { 151 | return 'Response content:' . "\n" . $session->getPage()->getContent() . "\n"; 152 | } catch (MinkException | WebDriverException | StreamReadException $exception) { 153 | return null; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/ServiceContainer/MinkDebugExtension.php: -------------------------------------------------------------------------------- 1 | loadStepFailureListener($container); 21 | 22 | $this->removeAllExistingLogsIfRequested($config); 23 | 24 | $container->setParameter('mink_debug.directory', $config['directory']); 25 | $container->setParameter('mink_debug.screenshot', $config['screenshot']); 26 | $container->setParameter('mink_debug.clean_start', $config['clean_start']); 27 | } 28 | 29 | public function configure(ArrayNodeDefinition $builder): void 30 | { 31 | $builder 32 | ->children() 33 | ->scalarNode('directory')->isRequired()->end() 34 | ->booleanNode('screenshot')->defaultFalse()->end() 35 | ->booleanNode('clean_start')->defaultTrue()->end() 36 | ->end(); 37 | } 38 | 39 | public function getConfigKey(): string 40 | { 41 | return 'fob_mink_debug'; 42 | } 43 | 44 | public function initialize(ExtensionManager $extensionManager): void 45 | { 46 | } 47 | 48 | public function process(ContainerBuilder $container): void 49 | { 50 | } 51 | 52 | private function loadStepFailureListener(ContainerBuilder $container): void 53 | { 54 | $definition = new Definition(FailedStepListener::class, [ 55 | new Reference('mink'), 56 | '%mink_debug.directory%', 57 | '%mink_debug.screenshot%', 58 | ]); 59 | 60 | $definition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG, ['priority' => 0]); 61 | 62 | $container->setDefinition('mink_debug.listener.step_failure', $definition); 63 | } 64 | 65 | /** 66 | * @param array $config 67 | */ 68 | private function removeAllExistingLogsIfRequested(array $config): void 69 | { 70 | if ($config['clean_start']) { 71 | array_map('unlink', glob($config['directory'] . '/*.html')); 72 | array_map('unlink', glob($config['directory'] . '/*.png')); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test-application/behat.yml.dist: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - Behat\MinkExtension\Context\MinkContext 6 | 7 | extensions: 8 | FriendsOfBehat\MinkDebugExtension: 9 | directory: %directory% 10 | clean_start: %clean_start% 11 | 12 | Behat\MinkExtension: 13 | sessions: 14 | default: 15 | goutte: ~ 16 | 17 | gherkin: 18 | filters: 19 | tags: "~@javascript" 20 | 21 | javascript: 22 | extensions: 23 | FriendsOfBehat\MinkDebugExtension: 24 | directory: %directory% 25 | screenshot: %screenshot% 26 | clean_start: %clean_start% 27 | 28 | DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~ 29 | 30 | Behat\MinkExtension: 31 | javascript_session: chrome 32 | sessions: 33 | chrome: 34 | chrome: 35 | api_url: http://127.0.0.1:9222 36 | validate_certificate: false 37 | show_auto: false 38 | 39 | gherkin: 40 | filters: 41 | tags: "@javascript" 42 | -------------------------------------------------------------------------------- /test-application/features/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing MinkDebugExtension 2 | In order to test MinkDebugExtension 3 | As a behat 4 | I want to download a page and fail 5 | 6 | Scenario: Downloading a page and failing 7 | When I go to "https://sylius.com" 8 | Then I select "Create failing test" from "Available steps" 9 | 10 | @javascript 11 | Scenario: Downloading a page and failing (Javascript session) 12 | When I go to "https://sylius.com" 13 | Then I select "Create failing test" from "Available steps" 14 | -------------------------------------------------------------------------------- /test-application/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfBehat/MinkDebugExtension/270e5aa5aef5358d81569a9a16eb2b3258314f9a/test-application/logs/.gitkeep --------------------------------------------------------------------------------