Skip to content

Commit e7fb8a1

Browse files
author
Sebastian Neubert
committed
Use Imagick to crop the image #1
2 parents 2fee00d + 335f403 commit e7fb8a1

File tree

4 files changed

+190
-150
lines changed

4 files changed

+190
-150
lines changed

LICENCE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2014 G+J Digital Products GmbH
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to
5+
deal in the Software without restriction, including without limitation the
6+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7+
sell copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19+
IN THE SOFTWARE.

module/VisualCeption.php

Lines changed: 152 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,152 @@
1-
<?php
2-
namespace Codeception\Module;
3-
4-
class VisualCeption extends \Codeception\Module
5-
{
6-
7-
private $referenceImageDir;
8-
9-
private $maximumDeviation = 0;
10-
11-
public function __construct ($config)
12-
{
13-
$result = parent::__construct($config);
14-
$this->init();
15-
return $result;
16-
}
17-
18-
public function _before (\Codeception\TestCase $test)
19-
{
20-
$this->test = $test;
21-
}
22-
23-
private function init ()
24-
{
25-
if (array_key_exists('maximumDeviation', $this->config)) {
26-
$this->maximumDeviation = $this->config["maximumDeviation"];
27-
}
28-
29-
if (array_key_exists('referenceImageDir', $this->config)) {
30-
$this->referenceImageDir = $this->config["referenceImageDir"];
31-
} else {
32-
throw new \RuntimeException("Reference image dir was not set, but is mandatory.");
33-
}
34-
35-
if (! is_dir($this->referenceImageDir)) {
36-
mkdir($this->referenceImageDir, 0777);
37-
}
38-
}
39-
40-
private function getCoordinates ($elementId)
41-
{
42-
$webDriver = $this->getModule("WebDriver")->webDriver;
43-
if (is_null($elementId)) {
44-
$elementId = 'body';
45-
}
46-
$imageCoords = array();
47-
$imageCoords['offset_x'] = (string) $webDriver->executeScript('var element = $( "' . $elementId . '" );var offset = element.offset();return offset.left;');
48-
$imageCoords['offset_y'] = (string) $webDriver->executeScript('var element = $( "' . $elementId . '" );var offset = element.offset();return offset.top;');
49-
$imageCoords['width'] = (string) $webDriver->executeScript('var element = $( "' . $elementId . '" );return element.width();');
50-
$imageCoords['height'] = (string) $webDriver->executeScript('var element = $( "' . $elementId . '" );return element.height();');
51-
52-
return $imageCoords;
53-
}
54-
55-
private function getScreenshotName ($identifier)
56-
{
57-
$caseName = str_replace('Cept.php', '', $this->test->getFileName());
58-
return $caseName . $identifier . "-element.png";
59-
}
60-
61-
private function getScreenshotPath ($identifier)
62-
{
63-
$debugDir = \Codeception\Configuration::logDir() . 'debug/tmp/';
64-
if (! is_dir($debugDir)) {
65-
mkdir($debugDir, 0777);
66-
}
67-
return $debugDir . $this->getScreenshotName($identifier);
68-
}
69-
70-
private function getExpectedScreenshotPath ($identifier)
71-
{
72-
return $this->referenceImageDir . $this->getScreenshotName($identifier);
73-
}
74-
75-
private function createScreenshot ($identifier, array $coords)
76-
{
77-
$webDriverModule = $this->getModule("WebDriver");
78-
$webDriver = $webDriverModule->webDriver;
79-
80-
$screenshotPath = \Codeception\Configuration::logDir() . 'debug/' . "fullscreenshot.tmp.png";
81-
$elementPath = $this->getScreenshotPath($identifier);
82-
83-
$webDriver->takeScreenshot($screenshotPath);
84-
85-
$screenShotImage = new \Imagick();
86-
$screenShotImage->readImage( $screenshotPath );
87-
$screenShotImage->cropImage( $coords['width'], $coords['height'], $coords['offset_x'], $coords['offset_y'] );
88-
$screenShotImage->writeImage( $elementPath );
89-
90-
unlink($screenshotPath);
91-
92-
return $elementPath;
93-
}
94-
95-
public function compareScreenshot ($identifier, $elementID = null)
96-
{
97-
$coords = $this->getCoordinates($elementID);
98-
$currentImagePath = $this->createScreenshot($identifier, $coords);
99-
100-
$compareResult = $this->compare($identifier);
101-
102-
unlink($this->getScreenshotPath($identifier));
103-
104-
$this->debug($compareResult);
105-
106-
if ($compareResult[1] > $this->maximumDeviation) {
107-
$compareScreenshotPath = $this->getDeviationScreenshotPath($identifier);
108-
$compareResult[0]->writeImage($compareScreenshotPath);
109-
$this->assertTrue(false, "The deviation of the taken screenshot is too high. See $compareScreenshotPath for a deviation screenshot.");
110-
}
111-
}
112-
113-
private function getDeviationScreenshotPath ($identifier)
114-
{
115-
$debugDir = \Codeception\Configuration::logDir() . 'debug/';
116-
return $debugDir . 'compare.' . $this->getScreenshotName($identifier);
117-
}
118-
119-
private function compare ($identifier)
120-
{
121-
$currentImagePath = $this->getScreenshotPath($identifier);
122-
$expectedImagePath = $this->getExpectedScreenshotPath($identifier);
123-
124-
if (! file_exists($expectedImagePath)) {
125-
copy($currentImagePath, $expectedImagePath);
126-
return array(null,0);
127-
} else {
128-
return $this->compareImages($expectedImagePath, $currentImagePath);
129-
}
130-
}
131-
132-
private function compareImages ($image1, $image2)
133-
{
134-
$imagick1 = new \Imagick($image1);
135-
$imagick2 = new \Imagick($image2);
136-
137-
$result = $imagick1->compareImages($imagick2, \Imagick::METRIC_MEANSQUAREERROR);
138-
$result[0]->setImageFormat("png");
139-
140-
$this->debug($result);
141-
142-
return $result;
143-
}
144-
}
1+
<?php
2+
3+
namespace Codeception\Module;
4+
5+
class VisualCeption extends \Codeception\Module
6+
{
7+
8+
private $referenceImageDir;
9+
10+
private $maximumDeviation = 0;
11+
12+
public function __construct ($config)
13+
{
14+
$result = parent::__construct($config);
15+
$this->init();
16+
return $result;
17+
}
18+
19+
public function _before (\Codeception\TestCase $test)
20+
{
21+
$this->test = $test;
22+
}
23+
24+
private function init ()
25+
{
26+
if (array_key_exists('maximumDeviation', $this->config)) {
27+
$this->maximumDeviation = $this->config["maximumDeviation"];
28+
}
29+
30+
if (array_key_exists('referenceImageDir', $this->config)) {
31+
$this->referenceImageDir = $this->config["referenceImageDir"];
32+
} else {
33+
throw new \RuntimeException("Reference image dir was not set, but is mandatory.");
34+
}
35+
36+
if (! is_dir($this->referenceImageDir)) {
37+
mkdir($this->referenceImageDir, 0666);
38+
}
39+
}
40+
41+
private function getCoordinates ($elementId)
42+
{
43+
$webDriver = $this->getModule("WebDriver")->webDriver;
44+
if (is_null($elementId)) {
45+
$elementId = 'body';
46+
}
47+
48+
$jQueryString = file_get_contents(__DIR__."/jquery.js");
49+
$webDriver->executeScript($jQueryString);
50+
$webDriver->executeScript('jQuery.noConflict();');
51+
52+
$imageCoords = array ();
53+
$imageCoords['offset_x'] = (string) $webDriver->executeScript('var element = jQuery( "' . $elementId . '" );var offset = element.offset();return offset.left;');
54+
$imageCoords['offset_y'] = (string) $webDriver->executeScript('var element = jQuery( "' . $elementId . '" );var offset = element.offset();return offset.top;');
55+
$imageCoords['width'] = (string) $webDriver->executeScript('var element = jQuery( "' . $elementId . '" );return element.width();');
56+
$imageCoords['height'] = (string) $webDriver->executeScript('var element = jQuery( "' . $elementId . '" );return element.height();');
57+
58+
return $imageCoords;
59+
}
60+
61+
private function getScreenshotName ($identifier)
62+
{
63+
$caseName = str_replace('Cept.php', '', $this->test->getFileName());
64+
return $caseName . '.' . $identifier . '.png';
65+
}
66+
67+
private function getScreenshotPath ($identifier)
68+
{
69+
$debugDir = \Codeception\Configuration::logDir() . 'debug/tmp/';
70+
if (! is_dir($debugDir)) {
71+
mkdir($debugDir, 0666);
72+
}
73+
return $debugDir . $this->getScreenshotName($identifier);
74+
}
75+
76+
private function getExpectedScreenshotPath ($identifier)
77+
{
78+
return $this->referenceImageDir . $this->getScreenshotName($identifier);
79+
}
80+
81+
private function createScreenshot ($identifier, array $coords)
82+
{
83+
$webDriverModule = $this->getModule("WebDriver");
84+
$webDriver = $webDriverModule->webDriver;
85+
86+
$screenshotPath = \Codeception\Configuration::logDir() . 'debug/' . "fullscreenshot.tmp.png";
87+
$elementPath = $this->getScreenshotPath($identifier);
88+
89+
$webDriver->takeScreenshot($screenshotPath);
90+
91+
$screenShotImage = new \Imagick();
92+
$screenShotImage->readImage( $screenshotPath );
93+
$screenShotImage->cropImage( $coords['width'], $coords['height'], $coords['offset_x'], $coords['offset_y'] );
94+
$screenShotImage->writeImage( $elementPath );
95+
96+
unlink($screenshotPath);
97+
98+
return $elementPath;
99+
}
100+
101+
public function compareScreenshot ($identifier, $elementID = null)
102+
{
103+
$coords = $this->getCoordinates($elementID);
104+
$currentImagePath = $this->createScreenshot($identifier, $coords);
105+
106+
$compareResult = $this->compare($identifier);
107+
108+
unlink($this->getScreenshotPath($identifier));
109+
110+
$this->debug($compareResult);
111+
112+
$deviation = round($compareResult[1] * 100, 2);
113+
114+
if ($deviation > $this->maximumDeviation) {
115+
$compareScreenshotPath = $this->getDeviationScreenshotPath($identifier);
116+
$compareResult[0]->writeImage($compareScreenshotPath);
117+
$this->assertTrue(false, "The deviation of the taken screenshot is too high (".$deviation."%).\nSee $compareScreenshotPath for a deviation screenshot.");
118+
}
119+
}
120+
121+
private function getDeviationScreenshotPath ($identifier)
122+
{
123+
$debugDir = \Codeception\Configuration::logDir() . 'debug/';
124+
return $debugDir . 'compare.' . $this->getScreenshotName($identifier);
125+
}
126+
127+
private function compare ($identifier)
128+
{
129+
$currentImagePath = $this->getScreenshotPath($identifier);
130+
$expectedImagePath = $this->getExpectedScreenshotPath($identifier);
131+
132+
if (! file_exists($expectedImagePath)) {
133+
copy($currentImagePath, $expectedImagePath);
134+
return array (null, 0);
135+
} else {
136+
return $this->compareImages($expectedImagePath, $currentImagePath);
137+
}
138+
}
139+
140+
private function compareImages ($image1, $image2)
141+
{
142+
$imagick1 = new \Imagick($image1);
143+
$imagick2 = new \Imagick($image2);
144+
145+
$result = $imagick1->compareImages($imagick2, \Imagick::METRIC_MEANSQUAREERROR);
146+
$result[0]->setImageFormat("png");
147+
148+
$this->debug($result);
149+
150+
return $result;
151+
}
152+
}

module/jquery.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readme.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
# VisualCeption
22
Visual regression tests integrated in [Codeception](http://codeception.com/).
33

4-
## Documenation todo
5-
* run twice
6-
* phantom css
7-
* example image
8-
* license
9-
* how it works
4+
This module can be used to compare the current representation of a website element with an expeted. It was written on the shoulders of codeception and integrates in a very easy way.
5+
6+
**Example**
7+
8+
![](http://www.thewebhatesme.com/VisualCeption/compare.png)
9+
10+
## How it works
11+
12+
VisualCeption uses a combination of the "make a screenshot" feature in webdriver, imagick and jquery to compare visual elements on a website. This comparison is done in five steps:
13+
14+
1. **Take a screenshot** of the full page using webdriver.
15+
2. **Calculate the position** and size of the selected element using jquery.
16+
3. **Crop the element** out of the full screenshot using imagick.
17+
4. **Compare the element** with an older version of the screenshot that has been proofed as valid using imagick. If no previous image exists the current image will be used fur future comparions. As an effect of this approach the test has to be **run twice** before it works.
18+
5. If the deviation is too high **throw an exception** that is caught by Codeception.
1019

1120
## Requirements
1221

0 commit comments

Comments
 (0)