diff --git a/.travis.yml b/.travis.yml index 67799c8..f0eb4e3 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,7 @@ php: - 5.5 env: - - CODECEPT_VERSION="1" - - CODECEPT_VERSION="2" - -matrix: - exclude: - - php: 5.3 - env: CODECEPT_VERSION="2" + - CODECEPT_VERSION="2.2" before_script: - printf "\n" | pecl install imagick diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..36246c8 --- /dev/null +++ b/composer.json @@ -0,0 +1,12 @@ +{ + "name": "digital-products-fork/codeception-visualception-wpbrowser", + "description": "A module for Codeception to test visual changes on web pages (with WPBrowser suppport via a webdriver parameter on VisualCeption module)", + "homepage": "https://github.com/DigitalProducts/codeception-module-visualception", + "license": "Apache-2.0", + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "classmap": ["module/"] + } +} diff --git a/module/ImageDeviationException.php b/module/ImageDeviationException.php index 454c4ac..5df3102 100755 --- a/module/ImageDeviationException.php +++ b/module/ImageDeviationException.php @@ -4,12 +4,14 @@ class ImageDeviationException extends \PHPUnit_Framework_ExpectationFailedException { + private $failedTest; private $expectedImage; private $currentImage; private $deviationImage; - public function __construct($message, $expectedImage, $currentImage, $deviationImage) + public function __construct($message, $test, $expectedImage, $currentImage, $deviationImage) { + $this->failedTest = $test; $this->deviationImage = $deviationImage; $this->currentImage = $currentImage; $this->expectedImage = $expectedImage; @@ -17,6 +19,11 @@ public function __construct($message, $expectedImage, $currentImage, $deviationI parent::__construct($message); } + public function getFailedTest( ) + { + return $this->failedTest; + } + public function getDeviationImage( ) { return $this->deviationImage; @@ -31,4 +38,4 @@ public function getExpectedImage() { return $this->expectedImage; } -} \ No newline at end of file +} diff --git a/module/VisualCeption.php b/module/VisualCeption.php index 4703ae8..09dcfb7 100755 --- a/module/VisualCeption.php +++ b/module/VisualCeption.php @@ -1,440 +1,509 @@ - - * @author Torsten Franz - * @author Sebastian Neubert - */ -class VisualCeption extends \Codeception\Module -{ - - private $referenceImageDir; - - /** - * This var represents the directory where the taken images are stored - * @var string - */ - private $currentImageDir; - - private $maximumDeviation = 0; - - private $webDriver = null; - private $webDriverModule = null; - - /** - * Create an object from VisualCeption Class - * - * @param array $config - * @return result - */ - public function __construct($config) - { - $result = parent::__construct($config); - $this->init(); - return $result; - } - - /** - * Event hook before a test starts - * - * @param \Codeception\TestCase $test - * @throws \Exception - */ - public function _before(\Codeception\TestCase $test) - { - if (!$this->hasModule("WebDriver")) { - throw new \Exception("VisualCeption uses the WebDriver. Please be sure that this module is activated."); - } - - $this->webDriverModule = $this->getModule("WebDriver"); - $this->webDriver = $this->webDriverModule->webDriver; - - $jQueryString = file_get_contents(__DIR__ . "/jquery.js"); - $this->webDriver->executeScript($jQueryString); - $this->webDriver->executeScript('jQuery.noConflict();'); - - $this->test = $test; - } - - public function getReferenceImageDir() - { - return $this->referenceImageDir; - } - - /** - * 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 - */ - public function seeVisualChanges($identifier, $elementID = null, $excludeElements = array()) - { - $excludeElements = (array)$excludeElements; - - $deviationResult = $this->getDeviation($identifier, $elementID, $excludeElements); - - if (!is_null($deviationResult["deviationImage"])) { - - // used for assertion counter in codeception / phpunit - $this->assertTrue(true); - - if ($deviationResult["deviation"] <= $this->maximumDeviation) { - $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); - } - } - } - - /** - * 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 - */ - public function dontSeeVisualChanges($identifier, $elementID = null, $excludeElements = array()) - { - $excludeElements = (array)$excludeElements; - - $deviationResult = $this->getDeviation($identifier, $elementID, $excludeElements); - - if (!is_null($deviationResult["deviationImage"])) { - - // used for assertion counter in codeception / phpunit - $this->assertTrue(true); - - if ($deviationResult["deviation"] > $this->maximumDeviation) { - $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); - } - } - } - - /** - * 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 = round($compareResult[1] * 100, 2); - - $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 - */ - private function init() - { - if (array_key_exists('maximumDeviation', $this->config)) { - $this->maximumDeviation = $this->config["maximumDeviation"]; - } - - if (array_key_exists('saveCurrentImageIfFailure', $this->config)) { - $this->saveCurrentImageIfFailure = (boolean) $this->config["saveCurrentImageIfFailure"]; - } - - if (array_key_exists('referenceImageDir', $this->config)) { - $this->referenceImageDir = $this->config["referenceImageDir"]; - } else { - $this->referenceImageDir = \Codeception\Configuration::dataDir() . 'VisualCeption/'; - } - - if (!is_dir($this->referenceImageDir)) { - $this->debug("Creating directory: $this->referenceImageDir"); - mkdir($this->referenceImageDir, 0777, true); - } - - if (array_key_exists('currentImageDir', $this->config)) { - $this->currentImageDir = $this->config["currentImageDir"]; - }else{ - $this->currentImageDir = \Codeception\Configuration::logDir() . 'debug/tmp/'; - } - } - - /** - * 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'; - } - - $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();'); - $imageCoords['height'] = (string)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).height();'); - - 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) - { - $caseName = str_replace('Cept.php', '', $this->test->getFileName()); - - $search = array('/', '\\'); - $replace = array('.', '.'); - $caseName = str_replace($search, $replace, $caseName); - - return $caseName . '.' . $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; - } -} \ No newline at end of file + + * @author Torsten Franz + * @author Sebastian Neubert + */ +class VisualCeption extends \Codeception\Module +{ + private $webDriverModuleName = "WebDriver"; + private $fullPageScreenshot = false; + private $referenceImageDir; + + /** + * This var represents the directory where the taken images are stored + * @var string + */ + private $currentImageDir; + + private $maximumDeviation = 0; + + private $webDriver = null; + private $webDriverModule = null; + + /** + * Event hook before a test starts + * + * @param \Codeception\TestInterface $test + * @throws \Exception + */ + public function _before(\Codeception\TestInterface $test) + { + if (!$this->hasModule($this->webDriverModuleName)) { + throw new \Exception("VisualCeption uses the " . $this->webDriverModuleName . ". Please be sure that this module is activated."); + } + + $this->webDriverModule = $this->getModule($this->webDriverModuleName); + $this->webDriver = $this->webDriverModule->webDriver; + + $jQueryString = file_get_contents(__DIR__ . "/jquery.js"); + $this->webDriver->executeScript($jQueryString); + $this->webDriver->executeScript('jQuery.noConflict();'); + + $this->test = $test; + } + + public function _after(\Codeception\TestInterface $test) + { + $this->test = null; + } + + public function getReferenceImageDir() + { + return $this->referenceImageDir; + } + + /** + * 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 string|array $removeElements Element name or array of Element names, which should not be incuded in the layout of the page + */ + public function seeVisualChanges($identifier, $elementID = null, $hideElements = array(), $removeElements = array()) + { + $excludeElements = (array)$excludeElements; + $removeElements = (array)$removeElements; + + $deviationResult = $this->getDeviation($identifier, $elementID, $excludeElements, $removeElements); + + if (!is_null($deviationResult["deviationImage"])) { + + // used for assertion counter in codeception / phpunit + $this->assertTrue(true); + + if ($deviationResult["deviation"] <= $this->maximumDeviation) { + $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->test, + $this->getExpectedScreenshotPath($identifier), + $this->getScreenshotPath($identifier), + $compareScreenshotPath); + } + } + } + + /** + * 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 string|array $removeElements Element name or array of Element names, which should not be incuded in the layout of the page + */ + public function dontSeeVisualChanges($identifier, $elementID = null, $excludeElements = array(), $removeElements = array()) + { + $excludeElements = (array)$excludeElements; + $removeElements = (array)$removeElements; + + $deviationResult = $this->getDeviation($identifier, $elementID, $excludeElements, $removeElements); + + if (!is_null($deviationResult["deviationImage"])) { + + // used for assertion counter in codeception / phpunit + $this->assertTrue(true); + + if ($deviationResult["deviation"] > $this->maximumDeviation) { + $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->test, + $this->getExpectedScreenshotPath($identifier), + $this->getScreenshotPath($identifier), + $compareScreenshotPath); + } + } + } + + /** + * 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'"); + } + + /** + * Remove an element from the layout by setting display to none + * + * @param $elementSelector String of jQuery Element selector, set display to none + */ + private function removeElement($elementSelector) + { + $this->webDriver->executeScript(' + if( jQuery("' . $elementSelector . '").length > 0 ) { + jQuery( "' . $elementSelector . '" ).css("display","none"); + } + '); + $this->debug("set display of element '$elementSelector' to 'none'"); + } + + /** + * Put back an element in the layout + * + * @param $elementSelector String of jQuery Element selector, remove display CSS + */ + private function putBackElement($elementSelector) + { + $this->webDriver->executeScript(' + if( jQuery("' . $elementSelector . '").length > 0 ) { + jQuery( "' . $elementSelector . '" ).css("display",""); + } + '); + $this->debug("removed display override of element '$elementSelector'"); + } + + /** + * 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(), array $removeElements = arRay()) + { + $coords = $this->getCoordinates($elementID); + $this->createScreenshot($identifier, $coords, $excludeElements, $removeElements); + + $compareResult = $this->compare($identifier); + + $deviation = round($compareResult[1] * 100, 2); + + $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 + */ + public function _initialize() + { + if (array_key_exists('webdriver', $this->config)) { + $this->webDriverModuleName = $this->config["webdriver"]; + } + + if (array_key_exists('fullPage', $this->config)) { + $this->fullPageScreenshot = $this->config["fullPage"]; + } + + if (array_key_exists('maximumDeviation', $this->config)) { + $this->maximumDeviation = $this->config["maximumDeviation"]; + } + + if (array_key_exists('saveCurrentImageIfFailure', $this->config)) { + $this->saveCurrentImageIfFailure = (boolean) $this->config["saveCurrentImageIfFailure"]; + } + + if (array_key_exists('referenceImageDir', $this->config)) { + $this->referenceImageDir = $this->config["referenceImageDir"]; + } else { + $this->referenceImageDir = \Codeception\Configuration::dataDir() . 'VisualCeption/'; + } + + if (!is_dir($this->referenceImageDir)) { + $this->debug("Creating directory: $this->referenceImageDir"); + mkdir($this->referenceImageDir, 0777, true); + } + + if (array_key_exists('currentImageDir', $this->config)) { + $this->currentImageDir = $this->config["currentImageDir"]; + }else{ + $this->currentImageDir = \Codeception\Configuration::logDir() . 'debug/tmp/'; + } + } + + /** + * 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'; + } + + $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();'); + $imageCoords['height'] = (string)$this->webDriver->executeScript('return jQuery( "' . $elementId . '" ).height();'); + + 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) + { + $caseName = str_replace('Cept.php', '', $this->test->getFileName()); + + $search = array('/', '\\'); + $replace = array('.', '.'); + $caseName = str_replace($search, $replace, $caseName); + + // Why do we put the full path????? + return /*$caseName . '.' . */$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 + * @param array $removeElements List of elements, which should not be included in the layout + * @return string Path of the current screenshot image + */ + private function createScreenshot($identifier, array $coords, array $excludeElements = array(), array $removeElements = 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->removeElementsForLayout($removeElements); + $this->webDriver->takeScreenshot($screenshotPath); + $this->resetHideElementsForScreenshot($excludeElements); + $this->resetRemoveElementsForLayout($removeElements); + + $screenShotImage = new \Imagick(); + $screenShotImage->readImage($screenshotPath); + + if(!$this->fullPageScreenshot) { + $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); + } + /** + * Remove the given elements with CSS display = none. Wait a second after hiding + * + * @param array $removeElements Array of strings, which should be not included in layout + */ + private function removeElementsForLAYOUT(array $removeElements) + { + foreach ($removeElements as $element) { + $this->removeElement($element); + } + $this->webDriverModule->wait(1); + } + + /** + * Reset removing the given elements with CSS display. Wait a second after reset display + * + * @param array $excludeElements array of strings, which should be included again + */ + private function resetRemoveElementsForLayout(array $removeElements) + { + foreach ($removeElements as $element) { + $this->putBackElement($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; + } +} diff --git a/module/VisualCeptionReporter.php b/module/VisualCeptionReporter.php index 8e6dafa..48bf14a 100755 --- a/module/VisualCeptionReporter.php +++ b/module/VisualCeptionReporter.php @@ -14,14 +14,7 @@ class VisualCeptionReporter extends \Codeception\Module private $referenceImageDir; - public function __construct($config) - { - $result = parent::__construct($config); - $this->init(); - return $result; - } - - private function init() + public function _initialize() { $this->debug("Initializing VisualCeptionReport"); @@ -42,7 +35,7 @@ private function init() } } - public function _beforeSuite() + public function _beforeSuite($settings = array()) { if (!$this->hasModule("VisualCeption")) { throw new \Exception("VisualCeptionReporter uses VisualCeption. Please be sure that this module is activated."); @@ -61,7 +54,7 @@ public function _afterSuite() $i = 0; ob_start(); - include_once $this->templateFile; + include $this->templateFile; $reportContent = ob_get_contents(); ob_clean(); @@ -69,10 +62,10 @@ public function _afterSuite() file_put_contents($this->logFile, $reportContent); } - public function _failed(\Codeception\TestCase $test, $fail) + public function _failed(\Codeception\TestInterface $test, $fail) { if ($fail instanceof ImageDeviationException) { $this->failed[] = $fail; } } -} \ No newline at end of file +} diff --git a/module/report/template.php b/module/report/template.php index b6adedb..5379b6e 100755 --- a/module/report/template.php +++ b/module/report/template.php @@ -8,7 +8,11 @@ +
+

getFailedTest()->getMetadata(); + echo \Codeception\Test\Descriptor::getTestSignature($failedTest->getFailedTest()) . " (" . $failedMetadata->getCurrent('env') . ")"; ?>

Deviation Image
@@ -23,9 +27,9 @@ Current Image
- +
- \ No newline at end of file + diff --git a/test/integration/codecept2.2.phar b/test/integration/codecept2.2.phar new file mode 100755 index 0000000..892cefd Binary files /dev/null and b/test/integration/codecept2.2.phar differ