123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- <?php
- namespace Codeception\Module;
- use Codeception\Module as CodeceptionModule;
- use Codeception\Test\Descriptor;
- use RemoteWebDriver;
- /**
- * Class VisualCeption
- *
- * @copyright Copyright (c) 2014 G+J Digital Products GmbH
- * @license MIT license, http://www.opensource.org/licenses/mit-license.php
- * @package Codeception\Module
- *
- * @author Nils Langner <langner.nils@guj.de>
- * @author Torsten Franz
- * @author Sebastian Neubert
- */
- class VisualCeption extends CodeceptionModule
- {
- protected $config = [
- 'maximumDeviation' => 0,
- 'saveCurrentImageIfFailure' => true,
- 'referenceImageDir' => 'VisualCeption/',
- 'currentImageDir' => 'debug/visual/',
- 'report' => false,
- 'module' => 'WebDriver'
- ];
-
- protected $saveCurrentImageIfFailure;
- private $referenceImageDir;
- /**
- * This var represents the directory where the taken images are stored
- * @var string
- */
- private $currentImageDir;
- private $maximumDeviation = 0;
- /**
- * @var RemoteWebDriver
- */
- private $webDriver = null;
- /**
- * @var WebDriver
- */
- private $webDriverModule = null;
- private $failed = array();
- private $logFile;
- private $templateVars = array();
- private $templateFile;
- public function _initialize()
- {
- $this->maximumDeviation = $this->config["maximumDeviation"];
- $this->saveCurrentImageIfFailure = (boolean)$this->config["saveCurrentImageIfFailure"];
- $this->referenceImageDir = codecept_data_dir() . $this->config["referenceImageDir"];
- if (!is_dir($this->referenceImageDir)) {
- $this->debug("Creating directory: $this->referenceImageDir");
- @mkdir($this->referenceImageDir, 0777, true);
- }
- $this->currentImageDir = codecept_output_dir() . $this->config["currentImageDir"];
- $this->_initVisualReport();
- }
- public function _afterSuite()
- {
- if (!$this->config['report']) {
- return;
- }
- $failedTests = $this->failed;
- $vars = $this->templateVars;
- $referenceImageDir = $this->referenceImageDir;
- $i = 0;
- ob_start();
- include_once $this->templateFile;
- $reportContent = ob_get_contents();
- ob_clean();
- $this->debug("Trying to store file (".$this->logFile.")");
- file_put_contents($this->logFile, $reportContent);
- }
- public function _failed(\Codeception\TestInterface $test, $fail)
- {
- if ($fail instanceof ImageDeviationException) {
- $this->failed[Descriptor::getTestAsString($test)] = $fail;
- }
- }
- /**
- * Event hook before a test starts
- *
- * @param \Codeception\TestInterface $test
- * @throws \Exception
- */
- public function _before(\Codeception\TestInterface $test)
- {
- if (!$this->hasModule($this->config['module'])) {
- throw new \Codeception\Exception\ConfigurationException("VisualCeption uses the WebDriver. Please ensure that this module is activated.");
- }
- if (!class_exists('Imagick')) {
- throw new \Codeception\Exception\ConfigurationException("VisualCeption requires ImageMagick PHP Extension but it was not installed");
- }
- $this->webDriverModule = $this->getModule($this->config['module']);
- $this->webDriver = $this->webDriverModule->webDriver;
- if ($this->webDriver->executeScript('return !window.jQuery;')) {
- $jQueryString = file_get_contents(__DIR__ . "/jquery.js");
- $this->webDriver->executeScript($jQueryString);
- $this->webDriver->executeScript('jQuery.noConflict();');
- }
- $this->test = $test;
- }
- /**
- * Compare the reference image with a current screenshot, identified by their indentifier name
- * and their element ID.
- *
- * @param string $identifier Identifies your test object
- * @param string $elementID DOM ID of the element, which should be screenshotted
- * @param string|array $excludeElements Element name or array of Element names, which should not appear in the screenshot
- * @param float $deviation
- */
- public function seeVisualChanges($identifier, $elementID = null, $excludeElements = array(), $deviation = null)
- {
- $excludeElements = (array)$excludeElements;
- if (!$deviation && !is_numeric($deviation)) {
- $deviation = (float)$this->maximumDeviation;
- }
- $deviationResult = $this->getDeviation($identifier, $elementID, $excludeElements);
- if (is_null($deviationResult["deviationImage"])) {
- return;
- }
- if ($deviationResult["deviation"] <= $deviation) {
- $compareScreenshotPath = $this->getDeviationScreenshotPath($identifier);
- $deviationResult["deviationImage"]->writeImage($compareScreenshotPath);
- throw new ImageDeviationException("The deviation of the taken screenshot is too low (" . $deviationResult["deviation"] . "%).\nSee $compareScreenshotPath for a deviation screenshot.",
- $this->getExpectedScreenshotPath($identifier),
- $this->getScreenshotPath($identifier),
- $compareScreenshotPath);
- }
- // used for assertion counter in codeception / phpunit
- $this->assertTrue(true);
- }
- /**
- * Compare the reference image with a current screenshot, identified by their indentifier name
- * and their element ID.
- *
- * @param string $identifier identifies your test object
- * @param string $elementID DOM ID of the element, which should be screenshotted
- * @param string|array $excludeElements string of Element name or array of Element names, which should not appear in the screenshot
- * @param float $deviation
- */
- public function dontSeeVisualChanges($identifier, $elementID = null, $excludeElements = array(), $deviation = null)
- {
- $excludeElements = (array)$excludeElements;
- if (!$deviation && !is_numeric($deviation)) {
- $deviation = (float)$this->maximumDeviation;
- }
- $deviationResult = $this->getDeviation($identifier, $elementID, $excludeElements);
- if (is_null($deviationResult["deviationImage"])) {
- return;
- }
- if ($deviationResult["deviation"] > $deviation) {
- $compareScreenshotPath = $this->getDeviationScreenshotPath($identifier);
- $deviationResult["deviationImage"]->writeImage($compareScreenshotPath);
- throw new ImageDeviationException("The deviation of the taken screenshot is too hight (" . $deviationResult["deviation"] . "%).\nSee $compareScreenshotPath for a deviation screenshot.",
- $this->getExpectedScreenshotPath($identifier),
- $this->getScreenshotPath($identifier),
- $compareScreenshotPath);
- }
- // used for assertion counter in codeception / phpunit
- $this->assertTrue(true);
- }
- /**
- * Hide an element to set the visibility to hidden
- *
- * @param $elementSelector String of jQuery Element selector, set visibility to hidden
- */
- private function hideElement($elementSelector)
- {
- $this->webDriver->executeScript('
- if( jQuery("' . $elementSelector . '").length > 0 ) {
- jQuery( "' . $elementSelector . '" ).css("visibility","hidden");
- }
- ');
- $this->debug("set visibility of element '$elementSelector' to 'hidden'");
- }
- /**
- * Show an element to set the visibility to visible
- *
- * @param $elementSelector String of jQuery Element selector, set visibility to visible
- */
- private function showElement($elementSelector)
- {
- $this->webDriver->executeScript('
- if( jQuery("' . $elementSelector . '").length > 0 ) {
- jQuery( "' . $elementSelector . '" ).css("visibility","visible");
- }
- ');
- $this->debug("set visibility of element '$elementSelector' to 'visible'");
- }
- /**
- * Compares the two images and calculate the deviation between expected and actual image
- *
- * @param string $identifier Identifies your test object
- * @param string $elementID DOM ID of the element, which should be screenshotted
- * @param array $excludeElements Element names, which should not appear in the screenshot
- * @return array Includes the calculation of deviation in percent and the diff-image
- */
- private function getDeviation($identifier, $elementID, array $excludeElements = array())
- {
- $coords = $this->getCoordinates($elementID);
- $this->createScreenshot($identifier, $coords, $excludeElements);
- $compareResult = $this->compare($identifier);
- $deviation = $compareResult[1] * 100;
- $this->debug("The deviation between the images is ". $deviation . " percent");
- return array (
- "deviation" => $deviation,
- "deviationImage" => $compareResult[0],
- "currentImage" => $compareResult['currentImage'],
- );
- }
- /**
- * Initialize the module and read the config.
- * Throws a runtime exception, if the
- * reference image dir is not set in the config
- *
- * @throws \RuntimeException
- */
- /**
- * Find the position and proportion of a DOM element, specified by it's ID.
- * The method inject the
- * JQuery Framework and uses the "noConflict"-mode to get the width, height and offset params.
- *
- * @param string $elementId DOM ID of the element, which should be screenshotted
- * @return array coordinates of the element
- */
- private function getCoordinates($elementId)
- {
- if (is_null($elementId)) {
- $elementId = 'body';
- }
- if ($this->webDriver->executeScript('return !window.jQuery;')) {
- $jQueryString = file_get_contents(__DIR__ . "/jquery.js");
- $this->webDriver->executeScript($jQueryString);
- $this->webDriver->executeScript('jQuery.noConflict();');
- }
- $imageCoords = array();
- $elementExists = (bool)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).length > 0;');
- if (!$elementExists) {
- throw new \Exception("The element you want to examine ('" . $elementId . "') was not found.");
- }
- $imageCoords['offset_x'] = (string)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).offset().left;');
- $imageCoords['offset_y'] = (string)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).offset().top;');
- $imageCoords['width'] = (string)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).width() * window.devicePixelRatio;');
- $imageCoords['height'] = (string)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).height() * window.devicePixelRatio;');
- return $imageCoords;
- }
- /**
- * Generates a screenshot image filename
- * it uses the testcase name and the given indentifier to generate a png image name
- *
- * @param string $identifier identifies your test object
- * @return string Name of the image file
- */
- private function getScreenshotName($identifier)
- {
- $signature = $this->test->getSignature();
- return str_replace(':', '_', $signature). '.' . $identifier . '.png';
- }
- /**
- * Returns the temporary path including the filename where a the screenshot should be saved
- * If the path doesn't exist, the method generate it itself
- *
- * @param string $identifier identifies your test object
- * @return string Path an name of the image file
- * @throws \RuntimeException if debug dir could not create
- */
- private function getScreenshotPath($identifier)
- {
- $debugDir = $this->currentImageDir;
- if (!is_dir($debugDir)) {
- $created = @mkdir($debugDir, 0777, true);
- if ($created) {
- $this->debug("Creating directory: $debugDir");
- } else {
- throw new \RuntimeException("Unable to create temporary screenshot dir ($debugDir)");
- }
- }
- return $debugDir . $this->getScreenshotName($identifier);
- }
- /**
- * Returns the reference image path including the filename
- *
- * @param string $identifier identifies your test object
- * @return string Name of the reference image file
- */
- private function getExpectedScreenshotPath($identifier)
- {
- return $this->referenceImageDir . $this->getScreenshotName($identifier);
- }
- /**
- * Generate the screenshot of the dom element
- *
- * @param string $identifier identifies your test object
- * @param array $coords Coordinates where the DOM element is located
- * @param array $excludeElements List of elements, which should not appear in the screenshot
- * @return string Path of the current screenshot image
- */
- private function createScreenshot($identifier, array $coords, array $excludeElements = array())
- {
- $screenShotDir = \Codeception\Configuration::logDir() . 'debug/';
- if( !is_dir($screenShotDir)) {
- mkdir($screenShotDir, 0777, true);
- }
- $screenshotPath = $screenShotDir . 'fullscreenshot.tmp.png';
- $elementPath = $this->getScreenshotPath($identifier);
- $this->hideElementsForScreenshot($excludeElements);
- $this->webDriver->takeScreenshot($screenshotPath);
- $this->resetHideElementsForScreenshot($excludeElements);
- $screenShotImage = new \Imagick();
- $screenShotImage->readImage($screenshotPath);
- $screenShotImage->cropImage($coords['width'], $coords['height'], $coords['offset_x'], $coords['offset_y']);
- $screenShotImage->writeImage($elementPath);
- unlink($screenshotPath);
- return $elementPath;
- }
- /**
- * Hide the given elements with CSS visibility = hidden. Wait a second after hiding
- *
- * @param array $excludeElements Array of strings, which should be not visible
- */
- private function hideElementsForScreenshot(array $excludeElements)
- {
- foreach ($excludeElements as $element) {
- $this->hideElement($element);
- }
- $this->webDriverModule->wait(1);
- }
- /**
- * Reset hiding the given elements with CSS visibility = visible. Wait a second after reset hiding
- *
- * @param array $excludeElements array of strings, which should be visible again
- */
- private function resetHideElementsForScreenshot(array $excludeElements)
- {
- foreach ($excludeElements as $element) {
- $this->showElement($element);
- }
- $this->webDriverModule->wait(1);
- }
- /**
- * Returns the image path including the filename of a deviation image
- *
- * @param $identifier identifies your test object
- * @return string Path of the deviation image
- */
- private function getDeviationScreenshotPath ($identifier, $alternativePrefix = '')
- {
- $debugDir = \Codeception\Configuration::logDir() . 'debug/';
- $prefix = ( $alternativePrefix === '') ? 'compare' : $alternativePrefix;
- return $debugDir . $prefix . $this->getScreenshotName($identifier);
- }
- /**
- * Compare two images by its identifiers.
- * If the reference image doesn't exists
- * the image is copied to the reference path.
- *
- * @param $identifier identifies your test object
- * @return array Test result of image comparison
- */
- private function compare($identifier)
- {
- $expectedImagePath = $this->getExpectedScreenshotPath($identifier);
- $currentImagePath = $this->getScreenshotPath($identifier);
- if (!file_exists($expectedImagePath)) {
- $this->debug("Copying image (from $currentImagePath to $expectedImagePath");
- copy($currentImagePath, $expectedImagePath);
- return array (null, 0, 'currentImage' => null);
- } else {
- return $this->compareImages($expectedImagePath, $currentImagePath);
- }
- }
- /**
- * Compares to images by given file path
- *
- * @param $image1 Path to the exprected reference image
- * @param $image2 Path to the current image in the screenshot
- * @return array Result of the comparison
- */
- private function compareImages($image1, $image2)
- {
- $this->debug("Trying to compare $image1 with $image2");
- $imagick1 = new \Imagick($image1);
- $imagick2 = new \Imagick($image2);
- $imagick1Size = $imagick1->getImageGeometry();
- $imagick2Size = $imagick2->getImageGeometry();
- $maxWidth = max($imagick1Size['width'], $imagick2Size['width']);
- $maxHeight = max($imagick1Size['height'], $imagick2Size['height']);
- $imagick1->extentImage($maxWidth, $maxHeight, 0, 0);
- $imagick2->extentImage($maxWidth, $maxHeight, 0, 0);
- try {
- $result = $imagick1->compareImages($imagick2, \Imagick::METRIC_MEANSQUAREERROR);
- $result[0]->setImageFormat('png');
- $result['currentImage'] = clone $imagick2;
- $result['currentImage']->setImageFormat('png');
- }
- catch (\ImagickException $e) {
- $this->debug("IMagickException! could not campare image1 ($image1) and image2 ($image2).\nExceptionMessage: " . $e->getMessage());
- $this->fail($e->getMessage() . ", image1 $image1 and image2 $image2.");
- }
- return $result;
- }
- protected function _initVisualReport()
- {
- if (!$this->config['report']) {
- return;
- }
- $this->logFile = \Codeception\Configuration::logDir() . 'vcresult.html';
- if (array_key_exists('templateVars', $this->config)) {
- $this->templateVars = $this->config["templateVars"];
- }
- if (array_key_exists('templateFile', $this->config)) {
- $this->templateFile = $this->config["templateFile"];
- } else {
- $this->templateFile = __DIR__ . "/report/template.php";
- }
- $this->debug( "VisualCeptionReporter: templateFile = " . $this->templateFile );
- }
- }
|