diff --git a/comparison b/comparison
index ee16f65..ae55bbb 100755
--- a/comparison
+++ b/comparison
@@ -5,9 +5,31 @@ require __DIR__ . '/vendor/autoload.php';
use Codappix\WebsiteComparison\Command\CompareCommand;
use Codappix\WebsiteComparison\Command\CreateBaseCommand;
+use Facebook\WebDriver\Chrome\ChromeDriver;
+use Facebook\WebDriver\Chrome\ChromeDriverService;
use Symfony\Component\Console\Application;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+
+$eventDispatcher = new EventDispatcher();
+
+$chromeDriver = (function () {
+ $chromeDriverService = new ChromeDriverService(
+ '/usr/lib/chromium-browser/chromedriver',
+ 9515,
+ [
+ '--port=9515',
+ '--headless',
+ ]
+ );
+
+ return ChromeDriver::start(null, $chromeDriverService);
+})();
$application = new Application();
-$application->add(new CreateBaseCommand());
-$application->add(new CompareCommand());
+$application->setDispatcher($eventDispatcher);
+
+// TODO: Use factory for commands, which injects event dispatcher and chrome driver?
+$application->add(new CreateBaseCommand($eventDispatcher, $chromeDriver));
+$application->add(new CompareCommand($eventDispatcher, $chromeDriver));
+
$application->run();
diff --git a/composer.json b/composer.json
index 950de22..69810b8 100644
--- a/composer.json
+++ b/composer.json
@@ -16,10 +16,11 @@
},
"require": {
"php": "^7.2",
- "php-imagick": "*",
+ "ext-imagick": "*",
"facebook/webdriver": "^1.6",
"symfony/console": "^4.1",
"symfony/process": "^4.1",
- "guzzlehttp/psr7": "^1.4"
+ "guzzlehttp/psr7": "^1.4",
+ "symfony/event-dispatcher": "^4.1"
}
}
diff --git a/src/Command/CompareCommand.php b/src/Command/CompareCommand.php
index a5a7699..69638a5 100644
--- a/src/Command/CompareCommand.php
+++ b/src/Command/CompareCommand.php
@@ -21,23 +21,39 @@ namespace Codappix\WebsiteComparison\Command;
* 02110-1301, USA.
*/
-use Codappix\WebsiteComparison\Service\ScreenshotCrawlerService;
-use Facebook\WebDriver\Chrome\ChromeDriver;
-use Facebook\WebDriver\Chrome\ChromeDriverService;
+use Codappix\WebsiteComparison\Service\Screenshot\CompareService;
+use Codappix\WebsiteComparison\Service\Screenshot\CrawlerService;
+use Codappix\WebsiteComparison\Service\Screenshot\Service;
+use Facebook\WebDriver\Remote\RemoteWebDriver;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Process\Exception\ProcessFailedException;
-use Symfony\Component\Process\Process;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
class CompareCommand extends Command
{
/**
- * @var Process
+ * @var EventDispatcherInterface
*/
- protected $chromeProcess;
+ protected $eventDispatcher;
+
+ /**
+ * @var RemoteWebDriver
+ */
+ protected $webDriver;
+
+ public function __construct(
+ EventDispatcherInterface $eventDispatcher,
+ RemoteWebDriver $webDriver
+ ) {
+ parent::__construct(null);
+
+ $this->eventDispatcher = $eventDispatcher;
+ $this->webDriver = $webDriver;
+ }
protected function configure()
{
@@ -85,35 +101,80 @@ class CompareCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output)
{
- $screenshotCrawler = new ScreenshotCrawlerService(
- $output,
- $this->getDriver(),
- $input->getArgument('baseUrl'),
+
+ $screenshotService = new Service(
+ $this->eventDispatcher,
$input->getOption('compareDir'),
$input->getOption('screenshotWidth')
);
- $hasDifferences = $screenshotCrawler->compare(
+
+ $screenshotCrawler = new CrawlerService(
+ $this->webDriver,
+ $screenshotService,
+ $input->getArgument('baseUrl')
+ );
+
+ $compareService = new CompareService(
+ $this->eventDispatcher,
+ $screenshotService,
$input->getOption('screenshotDir'),
$input->getOption('diffResultDir')
);
+ $this->registerEvents($output, $compareService);
- if ($hasDifferences) {
+ $screenshotCrawler->crawl();
+
+ if ($compareService->hasDifferences()) {
return 255;
}
}
- protected function getDriver(): ChromeDriver
+ protected function registerEvents(OutputInterface $output, CompareService $compareService)
{
- $chromeDriverService = new ChromeDriverService(
- '/usr/lib/chromium-browser/chromedriver',
- 9515,
- [
- '--port=9515',
- '--headless',
- ]
+ $this->eventDispatcher->addListener(
+ 'service.screenshot.created',
+ function (GenericEvent $event) use ($output, $compareService) {
+ $output->writeln(sprintf(
+ 'Comparing Screenshot for url "%s".',
+ $event->getArgument('url')
+ ));
+ $compareService->compareScreenshot(
+ $event->getArgument('screenshot')
+ );
+ }
);
- $driver = ChromeDriver::start(null, $chromeDriverService);
- return $driver;
+ if ($output->isVerbose()) {
+ $this->eventDispatcher->addListener(
+ 'service.screenshot.isSame',
+ function (GenericEvent $event) use ($output) {
+ $output->writeln(sprintf(
+ 'Screenshot "%s" is as expected.',
+ $event->getArgument('screenshot')
+ ));
+ }
+ );
+ }
+
+ $this->eventDispatcher->addListener(
+ 'service.screenshot.isDifferent',
+ function (GenericEvent $event) use ($output) {
+ $output->writeln(sprintf(
+ 'Screenshot "%s" is different, created diff at "%s".',
+ $event->getArgument('screenshot'),
+ $event->getArgument('diff')
+ ));
+ }
+ );
+
+ $this->eventDispatcher->addListener(
+ 'service.screenshot.error',
+ function (GenericEvent $event) use ($output) {
+ $output->writeln(sprintf(
+ '"%s"',
+ $event->getArgument('e')->getMessage()
+ ));
+ }
+ );
}
}
diff --git a/src/Command/CreateBaseCommand.php b/src/Command/CreateBaseCommand.php
index 9a054de..4bf4d57 100644
--- a/src/Command/CreateBaseCommand.php
+++ b/src/Command/CreateBaseCommand.php
@@ -21,23 +21,38 @@ namespace Codappix\WebsiteComparison\Command;
* 02110-1301, USA.
*/
-use Codappix\WebsiteComparison\Service\ScreenshotCrawlerService;
-use Facebook\WebDriver\Chrome\ChromeDriver;
-use Facebook\WebDriver\Chrome\ChromeDriverService;
+use Codappix\WebsiteComparison\Service\Screenshot\CrawlerService;
+use Codappix\WebsiteComparison\Service\Screenshot\Service;
+use Facebook\WebDriver\Remote\RemoteWebDriver;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Process\Exception\ProcessFailedException;
-use Symfony\Component\Process\Process;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
class CreateBaseCommand extends Command
{
/**
- * @var Process
+ * @var EventDispatcherInterface
*/
- protected $chromeProcess;
+ protected $eventDispatcher;
+
+ /**
+ * @var RemoteWebDriver
+ */
+ protected $webDriver;
+
+ public function __construct(
+ EventDispatcherInterface $eventDispatcher,
+ RemoteWebDriver $webDriver
+ ) {
+ parent::__construct(null);
+
+ $this->eventDispatcher = $eventDispatcher;
+ $this->webDriver = $webDriver;
+ }
protected function configure()
{
@@ -71,28 +86,45 @@ class CreateBaseCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output)
{
- $screenshotCrawler = new ScreenshotCrawlerService(
- $output,
- $this->getDriver(),
- $input->getArgument('baseUrl'),
+ $this->registerEvents($output);
+
+ $screenshotService = new Service(
+ $this->eventDispatcher,
$input->getOption('screenshotDir'),
$input->getOption('screenshotWidth')
);
+
+ $screenshotCrawler = new CrawlerService(
+ $this->webDriver,
+ $screenshotService,
+ $input->getArgument('baseUrl')
+ );
$screenshotCrawler->crawl();
}
- protected function getDriver(): ChromeDriver
+ protected function registerEvents(OutputInterface $output)
{
- $chromeDriverService = new ChromeDriverService(
- '/usr/lib/chromium-browser/chromedriver',
- 9515,
- [
- '--port=9515',
- '--headless',
- ]
- );
- $driver = ChromeDriver::start(null, $chromeDriverService);
+ if ($output->isVerbose()) {
+ $this->eventDispatcher->addListener(
+ 'service.screenshot.created',
+ function (GenericEvent $event) use ($output) {
+ $output->writeln(sprintf(
+ 'Created screenshot "%s" for url "%s".',
+ $event->getArgument('screenshot'),
+ $event->getArgument('url')
+ ));
+ }
+ );
+ }
- return $driver;
+ $this->eventDispatcher->addListener(
+ 'screenshot.service.error',
+ function (GenericEvent $event) use ($output) {
+ $output->writeln(sprintf(
+ '"%s"',
+ $event->getArgument('e')->getMessage()
+ ));
+ }
+ );
}
}
diff --git a/src/Service/Screenshot/CompareService.php b/src/Service/Screenshot/CompareService.php
new file mode 100644
index 0000000..77af4e8
--- /dev/null
+++ b/src/Service/Screenshot/CompareService.php
@@ -0,0 +1,152 @@
+
+ *
+ * This program 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 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
+
+class CompareService
+{
+ /**
+ * @var EventDispatcherInterface
+ */
+ protected $eventDispatcher;
+
+ /**
+ * @var Service
+ */
+ protected $screenshotService;
+
+ /**
+ * @var string
+ */
+ protected $compareDirectory = '';
+
+ /**
+ * @var string
+ */
+ protected $diffResultDir = '';
+
+ /**
+ * @var bool
+ */
+ protected $hasDifferences = false;
+
+ public function __construct(
+ EventDispatcherInterface $eventDispatcher,
+ Service $screenshotService,
+ string $compareDirectory,
+ string $diffResultDir
+ ) {
+ $this->eventDispatcher = $eventDispatcher;
+ $this->screenshotService = $screenshotService;
+ $this->compareDirectory = $this->screenshotService->convertReltiveFolder($compareDirectory);
+ $this->diffResultDir = $this->screenshotService->convertReltiveFolder($diffResultDir);
+ }
+
+ public function hasDifferences(): bool
+ {
+ return $this->hasDifferences;
+ }
+
+ public function compareScreenshot(string $screenshot)
+ {
+ try {
+ if ($this->doScreenshotsDiffer($screenshot)) {
+ $this->eventDispatcher->dispatch(
+ 'service.screenshot.isDifferent',
+ new GenericEvent('New Screenshot is different then base version.', [
+ 'screenshot' => $screenshot,
+ 'diff' => $this->getDiffFileName($screenshot)
+ ])
+ );
+ return;
+ }
+ } catch (\ImagickException $e) {
+ $this->eventDispatcher->dispatch(
+ 'service.screenshot.error',
+ new GenericEvent($e->getMessage(), [$e])
+ );
+ return;
+ }
+
+ $this->eventDispatcher->dispatch(
+ 'service.screenshot.isSame',
+ new GenericEvent('New Screenshot is the same as base version.', [
+ 'screenshot' => $screenshot,
+ ])
+ );
+ }
+
+ protected function doScreenshotsDiffer(string $screenshot): bool
+ {
+ $actualScreenshot = new \Imagick($screenshot);
+ $actualGeometry = $actualScreenshot->getImageGeometry();
+ $compareScreenshot = new \Imagick($this->getBaseScreenshot($screenshot));
+ $compareGeometry = $compareScreenshot->getImageGeometry();
+
+ if ($actualGeometry !== $compareGeometry) {
+ throw new \ImagickException(sprintf(
+ "Screenshots don't have an equal geometry. Should be %sx%s but is %sx%s",
+ $compareGeometry['width'],
+ $compareGeometry['height'],
+ $actualGeometry['width'],
+ $actualGeometry['height']
+ ));
+ }
+
+ $result = $actualScreenshot->compareImages(
+ $compareScreenshot,
+ \Imagick::METRIC_ROOTMEANSQUAREDERROR
+ );
+ if ($result[1] > 0) {
+ /** @var \Imagick $diffScreenshot */
+ $diffScreenshot = $result[0];
+ $diffScreenshot->setImageFormat('png');
+ $fileName = $this->getDiffFileName($screenshot);
+ $this->screenshotService->createDir(dirname($fileName));
+ file_put_contents($fileName, $diffScreenshot);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function getBaseScreenshot(string $compareScreenshot): string
+ {
+ return str_replace(
+ $this->screenshotService->getScreenshotDir(),
+ $this->compareDirectory,
+ $compareScreenshot
+ );
+ }
+
+ protected function getDiffFileName(string $screenshot): string
+ {
+ return str_replace(
+ $this->screenshotService->getScreenshotDir(),
+ $this->diffResultDir,
+ $screenshot
+ );
+ }
+}
diff --git a/src/Service/Screenshot/CrawlerService.php b/src/Service/Screenshot/CrawlerService.php
new file mode 100644
index 0000000..629ae08
--- /dev/null
+++ b/src/Service/Screenshot/CrawlerService.php
@@ -0,0 +1,123 @@
+
+ *
+ * This program 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 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use Codappix\WebsiteComparison\Model\UrlListDto;
+use Facebook\WebDriver\Remote\RemoteWebDriver;
+use Facebook\WebDriver\Remote\RemoteWebElement;
+use Facebook\WebDriver\WebDriverBy;
+use GuzzleHttp\Psr7\Uri;
+
+class CrawlerService
+{
+ /**
+ * @var RemoteWebDriver
+ */
+ protected $driver;
+
+ /**
+ * @var Service
+ */
+ protected $screenshotService;
+
+ /**
+ * @var string
+ */
+ protected $baseUrl = '';
+
+ public function __construct(
+ RemoteWebDriver $driver,
+ Service $screenshotService,
+ string $baseUrl
+ ) {
+ $this->driver = $driver;
+ $this->screenshotService = $screenshotService;
+ $this->baseUrl = rtrim($baseUrl, '/') . '/';
+ }
+
+ public function crawl()
+ {
+ $linkList = new UrlListDto();
+ $linkList->addUrl($this->baseUrl);
+
+ while ($url = $linkList->getNextUrl()) {
+ $this->driver->get($url);
+ $screenshotHeight = $this->driver->findElement(WebDriverBy::cssSelector('body'))
+ ->getSize()
+ ->getHeight();
+ $this->screenshotService->createScreenshot(
+ $this->driver->getCurrentURL(),
+ $screenshotHeight
+ );
+
+ $linkList->markUrlAsFinished($url);
+ array_map([$linkList, 'addUrl'], $this->fetchFurtherLinks(
+ $this->driver->findElements(WebDriverBy::cssSelector('a'))
+ ));
+ }
+ }
+
+ protected function fetchFurtherLinks(array $webElements): array
+ {
+ $links = [];
+ foreach ($webElements as $webElement) {
+ try {
+ $link = $this->fetchLinkFromElement($webElement);
+ } catch (\Exception $e) {
+ continue;
+ }
+
+ $links[] = $link;
+ }
+
+ return $links;
+ }
+
+ protected function fetchLinkFromElement(RemoteWebElement $element): string
+ {
+ $uri = null;
+ $href = $element->getAttribute('href');
+ if (is_string($href)) {
+ $uri = new Uri($href);
+ }
+
+ if ($uri === null) {
+ throw new \Exception('Did not get a Uri for element.', 1535530859);
+ }
+
+ if ($this->isInternalLink($uri)) {
+ return (string) $uri;
+ }
+
+ throw new \Exception('Was external link.', 1535639056);
+ }
+
+ protected function isInternalLink(Uri $uri): bool
+ {
+ $validHosts = [
+ '',
+ (new Uri($this->baseUrl))->getHost(),
+ ];
+
+ return in_array($uri->getHost(), $validHosts);
+ }
+}
diff --git a/src/Service/Screenshot/Service.php b/src/Service/Screenshot/Service.php
new file mode 100644
index 0000000..e8d1f28
--- /dev/null
+++ b/src/Service/Screenshot/Service.php
@@ -0,0 +1,132 @@
+
+ *
+ * This program 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 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use GuzzleHttp\Psr7\Uri;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
+use Symfony\Component\Process\Process;
+
+class Service
+{
+ /**
+ * @var EventDispatcherInterface
+ */
+ protected $eventDispatcher;
+
+ /**
+ * @var string
+ */
+ protected $screenshotDir = '';
+
+ /**
+ * @var int
+ */
+ protected $screenshotWidth = 3840;
+
+ public function __construct(
+ EventDispatcherInterface $eventDispatcher,
+ string $screenshotDir,
+ int $screenshotWidth
+ ) {
+ $this->eventDispatcher = $eventDispatcher;
+ $this->screenshotDir = $this->convertReltiveFolder($screenshotDir);
+ $this->screenshotWidth = $screenshotWidth;
+ }
+
+ public function createScreenshot(string $url, int $height): string
+ {
+ // TODO: Include width in screenshot dir.
+ // This enables to compare different resolutions
+ $screenshotTarget = $this->getScreenshotTarget($url);
+ $this->createDir($this->screenshotDir . dirname($screenshotTarget));
+ $completeScreenshotTarget = $this->screenshotDir . $screenshotTarget;
+
+ $screenshotProcess = new Process([
+ 'chromium-browser',
+ '--headless',
+ '--disable-gpu',
+ '--window-size=' . $this->screenshotWidth . ',' . $height,
+ '--screenshot=' . $completeScreenshotTarget,
+ $url
+ ]);
+ // TODO: Check for success
+ $screenshotProcess->run();
+
+ $this->eventDispatcher->dispatch(
+ 'service.screenshot.created',
+ new GenericEvent('Created Screenshot', [
+ 'screenshot' => $completeScreenshotTarget,
+ 'url' => $url,
+ ])
+ );
+
+ return $completeScreenshotTarget;
+ }
+
+ protected function getScreenshotTarget(string $url): string
+ {
+ $uri = new Uri($url);
+
+ return implode(
+ DIRECTORY_SEPARATOR,
+ array_filter(
+ [
+ $uri->getScheme(),
+ $uri->getHost(),
+ trim($uri->getPath(), '/'),
+ $uri->getQuery(),
+ ],
+ function (string $string) {
+ return trim($string, ' /') !== '';
+ }
+ )
+ ) . '.png';
+ }
+
+ public function getScreenshotDir(): string
+ {
+ return $this->screenshotDir;
+ }
+
+ /**
+ * @throws \Exception If folder could not be created.
+ */
+ public function createDir(string $dir)
+ {
+ if (!is_dir($dir)) {
+ mkdir($dir, 0777, true);
+ }
+
+ if (!is_dir($dir)) {
+ throw new \Exception('Could not create directory: "' . $dir . '".', 1535528875);
+ }
+ }
+
+ public function convertReltiveFolder(string $folder): string
+ {
+ return implode(DIRECTORY_SEPARATOR, [
+ dirname(dirname(dirname(dirname(__FILE__)))),
+ rtrim($folder, '/'),
+ ]) . DIRECTORY_SEPARATOR;
+ }
+}
diff --git a/src/Service/ScreenshotCrawlerService.php b/src/Service/ScreenshotCrawlerService.php
deleted file mode 100644
index 3cd5c88..0000000
--- a/src/Service/ScreenshotCrawlerService.php
+++ /dev/null
@@ -1,311 +0,0 @@
-
- *
- * This program 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 2
- * of the License, or (at your option) any later version.
- *
- * This program 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 this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
- * 02110-1301, USA.
- */
-
-use Codappix\WebsiteComparison\Model\UrlListDto;
-use Facebook\WebDriver\Remote\RemoteWebDriver;
-use Facebook\WebDriver\Remote\RemoteWebElement;
-use Facebook\WebDriver\WebDriverBy;
-use GuzzleHttp\Psr7\Uri;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Process\Process;
-
-/**
- *
- */
-class ScreenshotCrawlerService
-{
- /**
- * @var OutputInterface
- */
- protected $output;
-
- /**
- * @var RemoteWebDriver
- */
- protected $driver;
-
- /**
- * @var string
- */
- protected $baseUrl = '';
-
- /**
- * @var string
- */
- protected $screenshotDir = '';
-
- /**
- * @var int
- */
- protected $screenshotWidth = 3840;
-
- /**
- * @var string
- */
- protected $compareDirectory = '';
-
- /**
- * @var string
- */
- protected $diffResultDir = '';
-
- /**
- * @var bool
- */
- protected $hasDifferences = false;
-
- public function __construct(
- OutputInterface $output,
- RemoteWebDriver $driver,
- string $baseUrl,
- string $screenshotDir = 'output/',
- int $screenshotWidth = 3840
- ) {
- $this->output = $output;
- $this->driver = $driver;
- $this->baseUrl = rtrim($baseUrl, '/') . '/';
- $this->screenshotDir = $this->convertReltiveFolder($screenshotDir);
- $this->screenshotWidth = $screenshotWidth;
- }
-
- public function crawl()
- {
- $this->createDir($this->screenshotDir);
-
- $linkList = new UrlListDto();
- $linkList->addUrl($this->baseUrl);
-
- while ($url = $linkList->getNextUrl()) {
- $this->driver->get($url);
- $screenshotHeight = $this->driver->findElement(WebDriverBy::cssSelector('body'))
- ->getSize()
- ->getHeight();
- $createdScreenshot = $this->createScreenshot($this->driver->getCurrentURL(), $screenshotHeight);
-
- if ($this->compareDirectory !== '') {
- $this->compareScreenshot($createdScreenshot);
- }
-
- $linkList->markUrlAsFinished($url);
- array_map([$linkList, 'addUrl'], $this->fetchFurtherLinks(
- $this->driver->findElements(WebDriverBy::cssSelector('a'))
- ));
- }
- }
-
- public function compare(string $compareDirectory, string $diffResultDir): bool
- {
- // TODO: Check for existence of directory
- $this->compareDirectory = $this->convertReltiveFolder($compareDirectory);
- $this->diffResultDir = $this->convertReltiveFolder($diffResultDir);
- $this->crawl();
-
- return $this->hasDifferences;
- }
-
- /**
- * @throws \Exception If folder could not be created.
- */
- protected function createDir(string $dir)
- {
- if (!is_dir($dir)) {
- mkdir($dir, 0777, true);
- }
-
- if (!is_dir($dir)) {
- throw new \Exception('Could not create directory: "' . $dir . '".', 1535528875);
- }
- }
-
- protected function createScreenshot(string $url, int $height): string
- {
- // TODO: Include width in screenshot dir.
- // This enables to compare different resolutions
- $screenshotTarget = $this->getScreenshotTarget($url);
- $this->createDir($this->screenshotDir . dirname($screenshotTarget));
- $completeScreenshotTarget = $this->screenshotDir . $screenshotTarget;
-
- $screenshotProcess = new Process([
- 'chromium-browser',
- '--headless',
- '--disable-gpu',
- '--window-size=' . $this->screenshotWidth . ',' . $height,
- '--screenshot=' . $completeScreenshotTarget,
- $url
- ]);
- // TODO: Check for success
- $screenshotProcess->run();
-
- if ($this->output->isVerbose()) {
- $this->output->writeln(sprintf(
- 'Created screenshot "%s" for url "%s".',
- $completeScreenshotTarget,
- $url
- ));
- }
-
- return $completeScreenshotTarget;
- }
-
- protected function compareScreenshot(string $screenshot)
- {
- try {
- if ($this->doScreenshotsDiffer($screenshot)) {
- $this->hasDifferences = true;
- $this->output->writeln(sprintf(
- 'Screenshot "%s" is different then "%s". Diff was written to "%s".',
- $screenshot,
- $this->getBaseScreenshot($screenshot),
- $this->getDiffFileName($screenshot)
- ));
- return;
- }
- } catch (\ImagickException $e) {
- $this->output->writeln('' . $e->getMessage() . '');
- return;
- }
-
- if ($this->output->isVerbose()) {
- $this->output->writeln('Screenshot is same.');
- }
- }
-
- protected function doScreenshotsDiffer(string $screenshot): bool
- {
- $actualScreenshot = new \Imagick($screenshot);
- $actualGeometry = $actualScreenshot->getImageGeometry();
- $compareScreenshot = new \Imagick($this->getBaseScreenshot($screenshot));
- $compareGeometry = $compareScreenshot->getImageGeometry();
-
- if ($actualGeometry !== $compareGeometry) {
- throw new \ImagickException(sprintf(
- "Screenshots don't have an equal geometry. Should be %sx%s but is %sx%s",
- $compareGeometry['width'],
- $compareGeometry['height'],
- $actualGeometry['width'],
- $actualGeometry['height']
- ));
- }
-
- $result = $actualScreenshot->compareImages($compareScreenshot, \Imagick::METRIC_ROOTMEANSQUAREDERROR);
- if ($result[1] > 0) {
- /** @var \Imagick $diffScreenshot */
- $diffScreenshot = $result[0];
- $diffScreenshot->setImageFormat('png');
- $fileName = $this->getDiffFileName($screenshot);
- $this->createDir(dirname($fileName));
- file_put_contents($fileName, $diffScreenshot);
-
- return true;
- }
-
- return false;
- }
-
- protected function getBaseScreenshot(string $compareScreenshot): string
- {
- return str_replace(
- $this->screenshotDir,
- $this->compareDirectory,
- $compareScreenshot
- );
- }
-
- protected function getScreenshotTarget(string $url)
- {
- $uri = new Uri($url);
-
- return implode(
- DIRECTORY_SEPARATOR,
- array_filter(
- [
- $uri->getScheme(),
- $uri->getHost(),
- trim($uri->getPath(), '/'),
- $uri->getQuery(),
- ],
- function (string $string) {
- return trim($string, ' /') !== '';
- }
- )
- ) . '.png';
- }
-
- protected function fetchFurtherLinks(array $webElements): array
- {
- $links = [];
- foreach ($webElements as $webElement) {
- try {
- $link = $this->fetchLinkFromElement($webElement);
- } catch (\Exception $e) {
- continue;
- }
-
- $links[] = $link;
- }
-
- return $links;
- }
-
- protected function fetchLinkFromElement(RemoteWebElement $element): string
- {
- $uri = null;
- $href = $element->getAttribute('href');
- if (is_string($href)) {
- $uri = new Uri($href);
- }
-
- if ($uri === null) {
- throw new \Exception('Did not get a Uri for element.', 1535530859);
- }
-
- if ($this->isInternalLink($uri)) {
- return (string) $uri;
- }
-
- throw new \Exception('Was external link.', 1535639056);
- }
-
- protected function isInternalLink(Uri $uri): bool
- {
- $validHosts = [
- '',
- (new Uri($this->baseUrl))->getHost(),
- ];
-
- return in_array($uri->getHost(), $validHosts);
- }
-
- protected function convertReltiveFolder(string $folder): string
- {
- return implode(DIRECTORY_SEPARATOR, [
- dirname(dirname(dirname(__FILE__))),
- rtrim($folder, '/'),
- ]) . DIRECTORY_SEPARATOR;
- }
-
- protected function getDiffFileName(string $screenshot): string
- {
- return str_replace($this->screenshotDir, $this->diffResultDir, $screenshot);
- }
-}