TASK: Refactor code
Do not include output logic into services. Also do not couple comparison with crawling. Use Symfony Events to connect features.
This commit is contained in:
parent
eb6ff42f0c
commit
138789a4af
8 changed files with 572 additions and 360 deletions
26
comparison
26
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();
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
'<info>Comparing Screenshot for url "%s".</info>',
|
||||
$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(
|
||||
'<info>Screenshot "%s" is as expected.</info>',
|
||||
$event->getArgument('screenshot')
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$this->eventDispatcher->addListener(
|
||||
'service.screenshot.isDifferent',
|
||||
function (GenericEvent $event) use ($output) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>Screenshot "%s" is different, created diff at "%s".</error>',
|
||||
$event->getArgument('screenshot'),
|
||||
$event->getArgument('diff')
|
||||
));
|
||||
}
|
||||
);
|
||||
|
||||
$this->eventDispatcher->addListener(
|
||||
'service.screenshot.error',
|
||||
function (GenericEvent $event) use ($output) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>"%s"</error>',
|
||||
$event->getArgument('e')->getMessage()
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
if ($output->isVerbose()) {
|
||||
$this->eventDispatcher->addListener(
|
||||
'service.screenshot.created',
|
||||
function (GenericEvent $event) use ($output) {
|
||||
$output->writeln(sprintf(
|
||||
'<info>Created screenshot "%s" for url "%s".</info>',
|
||||
$event->getArgument('screenshot'),
|
||||
$event->getArgument('url')
|
||||
));
|
||||
}
|
||||
);
|
||||
$driver = ChromeDriver::start(null, $chromeDriverService);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
$this->eventDispatcher->addListener(
|
||||
'screenshot.service.error',
|
||||
function (GenericEvent $event) use ($output) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>"%s"</error>',
|
||||
$event->getArgument('e')->getMessage()
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
152
src/Service/Screenshot/CompareService.php
Normal file
152
src/Service/Screenshot/CompareService.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
namespace Codappix\WebsiteComparison\Service\Screenshot;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||
*
|
||||
* 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
|
||||
);
|
||||
}
|
||||
}
|
123
src/Service/Screenshot/CrawlerService.php
Normal file
123
src/Service/Screenshot/CrawlerService.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace Codappix\WebsiteComparison\Service\Screenshot;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
132
src/Service/Screenshot/Service.php
Normal file
132
src/Service/Screenshot/Service.php
Normal file
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace Codappix\WebsiteComparison\Service\Screenshot;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Codappix\WebsiteComparison\Service;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
|
||||
*
|
||||
* 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(
|
||||
'<info>Created screenshot "%s" for url "%s".</info>',
|
||||
$completeScreenshotTarget,
|
||||
$url
|
||||
));
|
||||
}
|
||||
|
||||
return $completeScreenshotTarget;
|
||||
}
|
||||
|
||||
protected function compareScreenshot(string $screenshot)
|
||||
{
|
||||
try {
|
||||
if ($this->doScreenshotsDiffer($screenshot)) {
|
||||
$this->hasDifferences = true;
|
||||
$this->output->writeln(sprintf(
|
||||
'<error>Screenshot "%s" is different then "%s". Diff was written to "%s".</error>',
|
||||
$screenshot,
|
||||
$this->getBaseScreenshot($screenshot),
|
||||
$this->getDiffFileName($screenshot)
|
||||
));
|
||||
return;
|
||||
}
|
||||
} catch (\ImagickException $e) {
|
||||
$this->output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->output->isVerbose()) {
|
||||
$this->output->writeln('<info>Screenshot is same.</info>');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue