[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n    push:\n    pull_request:\n        types: [opened, synchronize, edited, reopened]\n\njobs:\n    tests:\n        runs-on: ubuntu-latest\n        continue-on-error: false\n        name: \"PHP ${{ matrix.php }}\"\n\n        strategy:\n            fail-fast: false\n            matrix:\n                php:\n                    - '7.4'\n                    - '8.0'\n                    - '8.1'\n\n        steps:\n            - name: Checkout\n              uses: actions/checkout@v2\n\n            - name: Setup PHP\n              uses: shivammathur/setup-php@v2\n              with:\n                  coverage: none\n                  ini-values: \"memory_limit=-1\"\n                  php-version: ${{ matrix.php }}\n                  tools: composer:v2\n\n            - name: Run Chrome Headless\n              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 &\n\n            - name: Get Composer cache directory\n              id: composer-cache\n              run: echo \"::set-output name=dir::$(composer config cache-files-dir)\"\n\n            - name: Cache Composer\n              uses: actions/cache@v2\n              with:\n                  path: ${{ steps.composer-cache.outputs.dir }}\n                  key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json **/composer.lock') }}\n                  restore-keys: |\n                      ${{ runner.os }}-php-${{ matrix.php }}-composer-\n\n            - name: Install PHP dependencies\n              run: composer install --no-interaction\n\n            - name: Validate composer.json\n              run: composer validate --ansi --strict\n\n            - name: Run Behat\n              run: vendor/bin/behat --colors --strict --no-interaction -vvv -f progress\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor\n/composer.lock\n\n/behat.yml\n\n/test-application/logs/*\n!/test-application/logs/.gitkeep\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n\n### v2.0.1\n\n- [#35](https://github.com/FriendsOfBehat/MinkDebugExtension/issues/35) Ignore StreamReadException as well ([@pamil](https://github.com/pamil))\n\n### v2.0.0\n\n* Added support for PHP 8.0\n* Allowed taking screenshots with more drivers than just Selenium2Driver\n* Changed log files extension from `.log` to `.html`\n* Removed supplementary `upload-textfiles`, `upload-screenshots`, `wait-for-port` binaries\n* Renamed extension from `Lakion\\Behat\\MinkDebugExtension` to `FriendsOfBehat\\MinkDebugExtension`\n\n### v1.0.0\n\n* Initial release.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016-2020 Lakion\n              2020-2021 Kamil Kokot\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "MinkDebugExtension\n==================\n\n**MinkDebugExtension** is a Behat extension made for debugging and logging Mink related data after every failed step. \nIt is especially useful while running tests on continuous integration server like Travis.\nWhile using appropriate driver, you can also save screenshots just after the failure.\n\nInstallation\n------------\n\nAssuming you already have Composer:\n\n```bash\ncomposer require friends-of-behat/mink-debug-extension\n```\n\nThen you only need to configure your Behat profile:\n\n```yml\ndefault:\n    extensions:\n        FriendsOfBehat\\MinkDebugExtension:\n            directory: directory-where-to-save-logs\n```\n\nConfiguration reference\n-----------------------\n\nUnder `FriendsOfBehat\\MinkDebugExtension` there are three options to be configured:\n\n  - `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\n  - `screenshot` (default `false`) - whether to save screenshots if using supporting driver\n  - `clean_start` (default `true`) - whether to clean your existing logs on each Behat execution\n  \nTesting\n-------\n\nIn order to test the extensions run:\n\n```bash\ncomposer install\nbin/behat --strict\n```\n\nAuthors\n-------\n\nMinkDebugExtension was originally created by [Kamil Kokot](https://kamilkokot.com).\nSee the list of [contributors](https://github.com/FriendsOfBehat/MinkDebugExtension/contributors).\n"
  },
  {
    "path": "UPGRADE.md",
    "content": "# UPGRADE\n\n## FROM `1.x` TO `2.x`\n\n- Change required package from `lakion/mink-debug-extension` to `friends-of-behat/mink-debug-extension` in your `composer.json`\n- Change extension name from `Lakion\\Behat\\MinkDebugExtension` to `FriendsOfBehat\\MinkDebugExtension` in your `behat.yml`\n- Make sure you're not using binaries provided by `1.x` version of this library\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"friends-of-behat/mink-debug-extension\",\n    \"type\": \"behat-extension\",\n    \"description\": \"Debug extension for Behat\",\n    \"keywords\": [\n        \"debug\",\n        \"behat\",\n        \"mink\",\n        \"logging\"\n    ],\n    \"homepage\": \"https://github.com/FriendsOfBehat/MinkDebugExtension\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Kamil Kokot\",\n            \"email\": \"kamil@kokot.me\",\n            \"homepage\": \"https://kamilkokot.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=7.4\",\n        \"behat/behat\": \"^3.5\",\n        \"behat/mink-extension\": \"^2.3\"\n    },\n    \"require-dev\": {\n        \"behat/mink-goutte-driver\": \"^1.2\",\n        \"behat/mink-selenium2-driver\": \"^1.4\",\n        \"dmore/behat-chrome-extension\": \"^1.3\",\n        \"dmore/chrome-mink-driver\": \"^2.7\",\n        \"symfony/process\": \"^4.4 || ^5.2\"\n    },\n    \"extra\": {\n        \"branch-alias\": {\n            \"dev-master\": \"2.1-dev\"\n        }\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"FriendsOfBehat\\\\MinkDebugExtension\\\\\": \"src/\"\n        }\n    }\n}\n"
  },
  {
    "path": "features/bootstrap/FeatureContext.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Behat\\Behat\\Context\\Context;\nuse Behat\\Gherkin\\Node\\TableNode;\nuse Symfony\\Component\\Process\\PhpExecutableFinder;\nuse Symfony\\Component\\Process\\Process;\n\nfinal class FeatureContext implements Context\n{\n    /** @var string */\n    private string $phpBin;\n\n    /** @var array<string, string> */\n    private array $configuration = ['%clean_start%' => 'true'];\n\n    /** @var string */\n    private string $testApplicationDir;\n\n    /**\n     * @BeforeScenario\n     */\n    public function prepareProcess(): void\n    {\n        $phpFinder = new PhpExecutableFinder();\n        if (false === $php = $phpFinder->find()) {\n            throw new \\RuntimeException('Unable to find the PHP executable.');\n        }\n\n        $this->phpBin = $php;\n        $this->testApplicationDir = __DIR__ . '/../../test-application';\n    }\n\n    /**\n     * @Given there is following Behat extension configuration:\n     */\n    public function thereIsBehatExtensionConfiguration(TableNode $table): void\n    {\n        foreach ($table->getRowsHash() as $key => $value) {\n            $this->configuration['%' . $key . '%'] = $value;\n        }\n    }\n\n    /**\n     * @Given /configuration option \"([^\"]+?)\" is set to \"([^\"]+?)\"/\n     */\n    public function configurationOptionSet(string $key, string $value): void\n    {\n        $this->configuration['%' . $key . '%'] = $value;\n    }\n\n    /**\n     * @When /I run Behat with failing scenarios(?: using (.+?) profile)?/\n     */\n    public function iRunBehat(?string $profile = null): void\n    {\n        $this->createBehatConfigurationFile();\n\n        $this->doRunBehat($this->getExtraConfiguration($profile));\n\n        $this->deleteBehatConfigurationFile();\n    }\n\n    /**\n     * @Then there should be text log generated\n     */\n    public function thereShouldBeTextLogGenerated(): void\n    {\n        $logPattern = $this->testApplicationDir . '/' . $this->configuration['%directory%'] . '/*.html';\n\n        $logsAmount = count(glob($logPattern));\n        if ($logsAmount !== 1) {\n            throw new \\RuntimeException(sprintf('Expected 1 log file, found %d.', $logsAmount));\n        }\n    }\n\n    /**\n     * @Then a screenshot should be made\n     */\n    public function screenshotShouldBeMade(): void\n    {\n        $screenshotPattern = $this->testApplicationDir . '/' . $this->configuration['%directory%'] . '/*.png';\n\n        $screenshotsAmount = count(glob($screenshotPattern));\n        if ($screenshotsAmount !== 1) {\n            throw new \\RuntimeException(sprintf('Expected 1 screenshot, found %d.', $screenshotsAmount));\n        }\n    }\n\n    /**\n     * @Then a screenshot should not be made\n     */\n    public function screenshotShouldNotBeMade(): void\n    {\n        $screenshotPattern = $this->testApplicationDir . '/' . $this->configuration['%directory%'] . '/*.png';\n\n        $screenshotsAmount = count(glob($screenshotPattern));\n        if ($screenshotsAmount !== 0) {\n            throw new \\RuntimeException(sprintf('Expected no screenshots, found %d.', $screenshotsAmount));\n        }\n    }\n\n    private function createBehatConfigurationFile(): void\n    {\n        $behatConfiguration = strtr(\n            file_get_contents($this->testApplicationDir . '/behat.yml.dist'),\n            $this->configuration\n        );\n\n        file_put_contents($this->testApplicationDir . '/behat.yml', $behatConfiguration);\n    }\n\n    private function getExtraConfiguration(?string $profile): array\n    {\n        if (null !== $profile) {\n            return ['--profile=' . $profile];\n        }\n\n        return [];\n    }\n\n    private function doRunBehat(array $extraConfiguration): void\n    {\n        $arguments = array_merge(\n            [$this->phpBin, BEHAT_BIN_PATH, '--strict', '-vvv', '--no-interaction', '--lang=en'],\n            $extraConfiguration\n        );\n\n        $process = new Process($arguments, $this->testApplicationDir);\n        $process->start();\n        $process->wait();\n\n        printf(\"stdOut:\\n %s\\nstdErr:\\n%s\\n\", $process->getOutput(), $process->getErrorOutput());\n    }\n\n    private function deleteBehatConfigurationFile(): void\n    {\n        if (file_exists($behatFile = $this->testApplicationDir . '/behat.yml')) {\n            unlink($behatFile);\n        }\n    }\n}\n"
  },
  {
    "path": "features/mink_debug.feature",
    "content": "Feature: Logging debug data\n  In order to debug my Behat suites with ease\n  As a developer\n  I want to be able to access logs\n\n  Background:\n    Given there is following Behat extension configuration:\n        | directory  | logs |\n        | screenshot | true |\n\n  Scenario:\n     When I run Behat with failing scenarios\n     Then there should be text log generated\n\n  Scenario:\n     When I run Behat with failing scenarios using javascript profile\n     Then there should be text log generated\n      And a screenshot should be made\n\n  Scenario:\n    Given configuration option \"screenshot\" is set to \"false\"\n     When I run Behat with failing scenarios using javascript profile\n     Then there should be text log generated\n      And a screenshot should not be made\n"
  },
  {
    "path": "src/Listener/FailedStepListener.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace FriendsOfBehat\\MinkDebugExtension\\Listener;\n\nuse Behat\\Behat\\EventDispatcher\\Event\\AfterStepTested;\nuse Behat\\Behat\\EventDispatcher\\Event\\StepTested;\nuse Behat\\Mink\\Exception\\Exception as MinkException;\nuse Behat\\Mink\\Exception\\UnsupportedDriverActionException;\nuse Behat\\Mink\\Mink;\nuse Behat\\Mink\\Session;\nuse Behat\\Testwork\\Tester\\Result\\TestResult;\nuse DMore\\ChromeDriver\\StreamReadException;\nuse Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;\nuse WebDriver\\Exception as WebDriverException;\n\nfinal class FailedStepListener implements EventSubscriberInterface\n{\n    /**\n     * @var Mink\n     */\n    private Mink $mink;\n\n    /**\n     * @var string\n     */\n    private string $logDirectory;\n\n    /**\n     * @var bool\n     */\n    private bool $screenshot;\n\n    /**\n     * Used to ensure that screenshot and log comes from the same failed step.\n     *\n     * @var string\n     */\n    private string $currentDateAsString;\n\n    public function __construct(Mink $mink, string $logDirectory, bool $screenshot)\n    {\n        $this->mink = $mink;\n        $this->logDirectory = $logDirectory;\n        $this->screenshot = $screenshot;\n    }\n\n    /**\n     * @return array<string, array>\n     */\n    public static function getSubscribedEvents(): array\n    {\n        return [\n            StepTested::AFTER => ['logFailedStepInformations', -10],\n        ];\n    }\n\n    public function logFailedStepInformations(AfterStepTested $event): void\n    {\n        $testResult = $event->getTestResult();\n\n        if (!$testResult instanceof TestResult || TestResult::FAILED !== $testResult->getResultCode()) {\n            return;\n        }\n\n        if (!$this->hasEligibleMinkSession()) {\n            return;\n        }\n\n        $this->currentDateAsString = date('YmdHis');\n\n        $this->logPageContent();\n\n        if ($this->screenshot) {\n            $this->logScreenshot();\n        }\n    }\n\n    private function logPageContent(): void\n    {\n        $session = $this->getSession();\n\n        $log = sprintf('Current page: %d %s', $this->getStatusCode($session), $this->getCurrentUrl($session)) . \"\\n\";\n        $log .= $this->getResponseHeadersLogMessage($session);\n        $log .= $this->getResponseContentLogMessage($session);\n\n        $this->saveLog($log, 'html');\n    }\n\n    private function logScreenshot(): void\n    {\n        $session = $this->getSession();\n\n        try {\n            $this->saveLog($session->getScreenshot(), 'png');\n        } catch (UnsupportedDriverActionException | WebDriverException $exception) {}\n    }\n\n    private function saveLog(string $content, string $type): void\n    {\n        $path = sprintf(\"%s/behat-%s.%s\", $this->logDirectory, $this->currentDateAsString, $type);\n\n        if (file_put_contents($path, $content) === false) {\n            throw new \\RuntimeException(sprintf('Failed while trying to write log in \"%s\".', $path));\n        }\n    }\n\n    private function getSession(?string $name = null): Session\n    {\n        return $this->mink->getSession($name);\n    }\n\n    private function hasEligibleMinkSession(?string $name = null): bool\n    {\n        $name = $name ?: $this->mink->getDefaultSessionName();\n\n        return $this->mink->hasSession($name) && $this->mink->isSessionStarted($name);\n    }\n\n    private function getStatusCode(Session $session): ?int\n    {\n        try {\n            return $session->getStatusCode();\n        } catch (MinkException | WebDriverException | StreamReadException $exception) {\n            return null;\n        }\n    }\n\n    private function getCurrentUrl(Session $session): ?string\n    {\n        try {\n            return $session->getCurrentUrl();\n        } catch (MinkException | WebDriverException | StreamReadException $exception) {\n            return null;\n        }\n    }\n\n    private function getResponseHeadersLogMessage(Session $session): ?string\n    {\n        try {\n            return 'Response headers:' . \"\\n\" . print_r($session->getResponseHeaders(), true) . \"\\n\";\n        } catch (MinkException | WebDriverException | StreamReadException $exception) {\n            return null;\n        }\n    }\n\n    private function getResponseContentLogMessage(Session $session): ?string\n    {\n        try {\n            return 'Response content:' . \"\\n\" . $session->getPage()->getContent() . \"\\n\";\n        } catch (MinkException | WebDriverException | StreamReadException $exception) {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/ServiceContainer/MinkDebugExtension.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace FriendsOfBehat\\MinkDebugExtension\\ServiceContainer;\n\nuse Behat\\Testwork\\EventDispatcher\\ServiceContainer\\EventDispatcherExtension;\nuse Behat\\Testwork\\ServiceContainer\\Extension as ExtensionInterface;\nuse Behat\\Testwork\\ServiceContainer\\ExtensionManager;\nuse FriendsOfBehat\\MinkDebugExtension\\Listener\\FailedStepListener;\nuse Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition;\nuse Symfony\\Component\\DependencyInjection\\ContainerBuilder;\nuse Symfony\\Component\\DependencyInjection\\Definition;\nuse Symfony\\Component\\DependencyInjection\\Reference;\n\nfinal class MinkDebugExtension implements ExtensionInterface\n{\n    public function load(ContainerBuilder $container, array $config): void\n    {\n        $this->loadStepFailureListener($container);\n\n        $this->removeAllExistingLogsIfRequested($config);\n\n        $container->setParameter('mink_debug.directory', $config['directory']);\n        $container->setParameter('mink_debug.screenshot', $config['screenshot']);\n        $container->setParameter('mink_debug.clean_start', $config['clean_start']);\n    }\n\n    public function configure(ArrayNodeDefinition $builder): void\n    {\n        $builder\n            ->children()\n                ->scalarNode('directory')->isRequired()->end()\n                ->booleanNode('screenshot')->defaultFalse()->end()\n                ->booleanNode('clean_start')->defaultTrue()->end()\n            ->end();\n    }\n\n    public function getConfigKey(): string\n    {\n        return 'fob_mink_debug';\n    }\n\n    public function initialize(ExtensionManager $extensionManager): void\n    {\n    }\n\n    public function process(ContainerBuilder $container): void\n    {\n    }\n\n    private function loadStepFailureListener(ContainerBuilder $container): void\n    {\n        $definition = new Definition(FailedStepListener::class, [\n            new Reference('mink'),\n            '%mink_debug.directory%',\n            '%mink_debug.screenshot%',\n        ]);\n\n        $definition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG, ['priority' => 0]);\n\n        $container->setDefinition('mink_debug.listener.step_failure', $definition);\n    }\n\n    /**\n     * @param array<string, mixed> $config\n     */\n    private function removeAllExistingLogsIfRequested(array $config): void\n    {\n        if ($config['clean_start']) {\n            array_map('unlink', glob($config['directory'] . '/*.html'));\n            array_map('unlink', glob($config['directory'] . '/*.png'));\n        }\n    }\n}\n"
  },
  {
    "path": "test-application/behat.yml.dist",
    "content": "default:\n    suites:\n        default:\n            contexts:\n                - Behat\\MinkExtension\\Context\\MinkContext\n\n    extensions:\n        FriendsOfBehat\\MinkDebugExtension:\n            directory: %directory%\n            clean_start: %clean_start%\n\n        Behat\\MinkExtension:\n            sessions:\n                default:\n                    goutte: ~\n\n    gherkin:\n        filters:\n            tags: \"~@javascript\"\n\njavascript:\n    extensions:\n        FriendsOfBehat\\MinkDebugExtension:\n            directory: %directory%\n            screenshot: %screenshot%\n            clean_start: %clean_start%\n\n        DMore\\ChromeExtension\\Behat\\ServiceContainer\\ChromeExtension: ~\n\n        Behat\\MinkExtension:\n            javascript_session: chrome\n            sessions:\n                chrome:\n                    chrome:\n                        api_url: http://127.0.0.1:9222\n                        validate_certificate: false\n            show_auto: false\n\n    gherkin:\n        filters:\n            tags: \"@javascript\"\n"
  },
  {
    "path": "test-application/features/test.feature",
    "content": "Feature: Testing MinkDebugExtension\n  In order to test MinkDebugExtension\n  As a behat\n  I want to download a page and fail\n\n  Scenario: Downloading a page and failing\n     When I go to \"https://sylius.com\"\n     Then I select \"Create failing test\" from \"Available steps\"\n\n  @javascript\n  Scenario: Downloading a page and failing (Javascript session)\n    When I go to \"https://sylius.com\"\n    Then I select \"Create failing test\" from \"Available steps\"\n"
  },
  {
    "path": "test-application/logs/.gitkeep",
    "content": ""
  }
]