Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.


namespace Moodle\BehatExtension\Output\Formatter;

use Behat\Behat\EventDispatcher\Event\AfterStepTested;
use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested;
use Behat\Behat\EventDispatcher\Event\BeforeStepTested;
use Behat\Testwork\Output\Formatter;
use Behat\Testwork\Output\Printer\OutputPrinter;

// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod

/**
 * Feature step counter for distributing features between parallel runs.
 *
 * Use it with --dry-run (and any other selectors combination) to
 * get the results quickly.
 *
 * @package core
 * @copyright  2016 onwards Rajesh Taneja
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class MoodleScreenshotFormatter implements Formatter {

    /** @var OutputPrinter */
    private $printer;

    /** @var array */
    private $parameters;

    /** @var string */
    private $name;

    /** @var string */
    private $description;

    /** @var int The scenario count */
    protected static $currentscenariocount = 0;

    /** @var int The step count within the current scenario */
    protected static $currentscenariostepcount = 0;

    /**
     * If we are saving any kind of dump on failure we should use the same parent dir during a run.
     *
     * @var The parent dir name
     */
    protected static $faildumpdirname = false;

    /**
     * Initializes formatter.
     *
     * @param string        $name
     * @param string        $description
     * @param array         $parameters
     * @param OutputPrinter $printer
     */
    public function __construct($name, $description, array $parameters, OutputPrinter $printer) {
        $this->name = $name;
        $this->description = $description;
        $this->parameters = $parameters;
        $this->printer = $printer;
    }

    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * @return array The event names to listen to
     */
    public static function getSubscribedEvents() {
        return [
            'tester.scenario_tested.before'    => 'beforeScenario',
            'tester.step_tested.before'        => 'beforeStep',
            'tester.step_tested.after'         => 'afterStep',
        ];
    }

    /**
     * Returns formatter name.
     *
     * @return string
     */
    public function getName() {
        return $this->name;
    }

    /**
     * Returns formatter description.
     *
     * @return string
     */
    public function getDescription() {
        return $this->description;
    }

    /**
     * Returns formatter output printer.
     *
     * @return OutputPrinter
     */
    public function getOutputPrinter() {
        return $this->printer;
    }

    /**
     * Sets formatter parameter.
     *
     * @param string $name
     * @param mixed  $value
     */
    public function setParameter($name, $value) {
        $this->parameters[$name] = $value;
    }

    /**
     * Returns parameter name.
     *
     * @param string $name
     * @return mixed
     */
    public function getParameter($name) {
        return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
    }

    /**
     * Reset currentscenariostepcount
     *
     * @param BeforeScenarioTested $event
     */
    public function beforeScenario(BeforeScenarioTested $event) {

        self::$currentscenariostepcount = 0;
        self::$currentscenariocount++;
    }

    /**
     * Increment currentscenariostepcount
     *
     * @param BeforeStepTested $event
     */
    public function beforeStep(BeforeStepTested $event) {
        self::$currentscenariostepcount++;
    }

    /**
     * Take screenshot after step is executed.    Behat\Behat\Event\html
     *
     * @param AfterStepTested $event
     */
    public function afterStep(AfterStepTested $event) {
        $behathookcontext = $event->getEnvironment()->getContext('behat_hooks');

        $formats = $this->getParameter('formats');
        $formats = explode(',', $formats);

        // Take screenshot.
        if (in_array('image', $formats)) {
            $this->take_screenshot($event, $behathookcontext);
        }

        // Save html content.
        if (in_array('html', $formats)) {
            $this->take_contentdump($event, $behathookcontext);
        }
    }

    /**
     * Return screenshot directory where all screenshots will be saved.
     *
     * @return string
     */
    protected function get_run_screenshot_dir() {
        global $CFG;

        if (self::$faildumpdirname) {
            return self::$faildumpdirname;
        }

        // If output_path is set then use output_path else use faildump_path.
        if ($this->getOutputPrinter()->getOutputPath()) {
            $screenshotpath = $this->getOutputPrinter()->getOutputPath();
        } else if ($CFG->behat_faildump_path) {
            $screenshotpath = $CFG->behat_faildump_path;
        } else {
            // It should never reach here.
            throw new FormatterException('You should specify --out "SOME/PATH" for moodle_screenshot format');
        }

        if ($this->getParameter('dir_permissions')) {
            $dirpermissions = $this->getParameter('dir_permissions');
        } else {
            $dirpermissions = 0777;
        }

        // All the screenshot dumps should be in the same parent dir.
        self::$faildumpdirname = $screenshotpath . DIRECTORY_SEPARATOR . date('Ymd_His');

        if (!is_dir(self::$faildumpdirname) && !mkdir(self::$faildumpdirname, $dirpermissions, true)) {
            // It shouldn't, we already checked that the directory is writable.
            throw new FormatterException(sprintf(
                'No directories can be created inside %s, check the directory permissions.', $screenshotpath
            ));
        }

        return self::$faildumpdirname;
    }

    /**
     * Take screenshot when a step fails.
     *
     * @throws Exception
     * @param AfterStepTested $event
     * @param Context $context
     */
    protected function take_screenshot(AfterStepTested $event, $context) {
< // Goutte can't save screenshots.
> // BrowserKit can't save screenshots.
if ($context->getMink()->isSessionStarted($context->getMink()->getDefaultSessionName())) {
< if (get_class($context->getMink()->getSession()->getDriver()) === 'Behat\Mink\Driver\GoutteDriver') {
> if (get_class($context->getMink()->getSession()->getDriver()) === 'Behat\Mink\Driver\BrowserKitDriver') {
return false; } list ($dir, $filename) = $this->get_faildump_filename($event, 'png'); $context->saveScreenshot($filename, $dir); } } /** * Take a dump of the page content when a step fails. * * @throws Exception * @param AfterStepTested $event * @param \Behat\Context\Context\Context $context */ protected function take_contentdump(AfterStepTested $event, $context) { list ($dir, $filename) = $this->get_faildump_filename($event, 'html'); $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w'); fwrite($fh, $context->getMink()->getSession()->getPage()->getContent()); fclose($fh); } /** * Determine the full pathname to store a failure-related dump. * * This is used for content such as the DOM, and screenshots. * * @param AfterStepTested $event * @param String $filetype The file suffix to use. Limited to 4 chars. */ protected function get_faildump_filename(AfterStepTested $event, $filetype) { // Make a directory for the scenario. $featurename = $event->getFeature()->getTitle(); $featurename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $featurename); if ($this->getParameter('dir_permissions')) { $dirpermissions = $this->getParameter('dir_permissions'); } else { $dirpermissions = 0777; } $dir = $this->get_run_screenshot_dir(); // We want a i-am-the-scenario-title format. $dir = $dir . DIRECTORY_SEPARATOR . self::$currentscenariocount . '-' . $featurename; if (!is_dir($dir) && !mkdir($dir, $dirpermissions, true)) { // We already checked that the directory is writable. This should not fail. throw new FormatterException(sprintf( 'No directories can be created inside %s, check the directory permissions.', $dir )); } // The failed step text. // We want a stepno-i-am-the-failed-step.$filetype format. $filename = $event->getStep()->getText(); $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename); $filename = self::$currentscenariostepcount . '-' . $filename; // File name limited to 255 characters. Leaving 4 chars for the file // extension as we allow .png for images and .html for DOM contents. $filename = substr($filename, 0, 250) . '.' . $filetype; return [$dir, $filename]; } }