diff --git a/README.md b/README.md index 446d3d2..b732bc9 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,6 @@ Percy https://percy.io module for Codeception ## Requirements -### `v1.1.x` -- Node.js `>=10.0.0` -- PHP `>= 7.2` -- Composer `v1` - -### `v2.0.x` -- Node.js `>=10.0.0` -- PHP `>= 7.3` -- Composer `v2` - -### `v3.0.x` -- Node.js `>=12.0.0` -- PHP `>= 7.3` -- Composer `v2` - -### `v4.0.x` - Node.js `>=14.0.0` - PHP `>= 7.4` - Composer `v2` @@ -27,19 +11,6 @@ Percy https://percy.io module for Codeception composer require --dev tr33m4n/codeception-module-percy ``` -## Upgrading -### `v1.0.x` to `v1.1.x` -The way in which the Percy agent is started and stopped in `v1.1.x` changes significantly from `v1.0.x`. You no longer need to prefix your Codeception run command with `npx percy exec --` :tada: - -### `v1.1.x` to `v2.0.x` -`v2.0.x` only supports PHP `7.3` and composer `v2` or later, however the base functionality is the same as `v1.1.x` - -### `v2.0.x` to `v3.0.x` -`v3.0.x` only supports Node `>=12`. Due to a typical PHP based platform using Composer not blocking the installation of this version if you have a lesser Node version, caution is advised! - -### `v3.0.x` to `v4.0.x` -`v4.0.x` only support Node `>=14` and PHP `>=7.4`. The `driver` config parameter has also been dropped as this has always explicitly required the Codeception WebDriver module - ## Example Configuration The following example configuration assumes the `WebDriver` module has been configured correctly for your test suite ```yaml @@ -58,19 +29,19 @@ modules: ``` ### Configuration Options -| Parameter | Type | Default | Description | -| --------------------------------- | -------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `snapshotEndpoint` | string | `http://localhost:5338` | The endpoint used for operations within the Percy agent | -| `snapshotPath` | string | `percy/snapshot` | The path relative to the agent endpoint to post a snapshot to | -| `snapshotConfig` | object | `{}` | Additional configuration to pass to the "snapshot" functionality | -| `snapshotConfig.percyCSS` | string | `null` | Percy specific CSS to apply to the "snapshot" | -| `snapshotConfig.minHeight` | int | `null` | Minimum height of the resulting "snapshot" in pixels | -| `snapshotConfig.enableJavaScript` | bool | `false` | Enable JavaScript in the Percy rendering environment | -| `snapshotConfig.widths` | array | `null` | An array of integers representing the browser widths at which you want to take snapshots | -| `serializeConfig` | object | `{"enableJavaScript": true}` | Additional configuration to pass to the `PercyDOM.serialize` method injected into the web driver DOM | -| `snapshotServerTimeout` | int\|null | `null` | [debug] The length of the time the Percy snapshot server will listen for incoming snapshots and send on to Percy.io (the amount of time needed to send all snapshots after a successful test suite run). No timeout is set by default | -| `throwOnAdapterError` | bool | `false` | [debug] Throw exception on adapter error | -| `cleanSnapshotStorage` | bool | `false` | [debug] Clean stored snapshot HTML after run | +| Parameter | Type | Default | Description | +|------------------------------------|------------|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `snapshotBaseUrl` | string | `http://localhost:5338` | The base URL used for operations within the Percy agent | +| `snapshotPath` | string | `percy/snapshot` | The path relative to the agent endpoint to post a snapshot to | +| `snapshotConfig` | object | `{}` | Additional configuration to pass to the "snapshot" functionality | +| `snapshotConfig.percyCSS` | string | `null` | Percy specific CSS to apply to the "snapshot" | +| `snapshotConfig.minHeight` | int | `null` | Minimum height of the resulting "snapshot" in pixels | +| `snapshotConfig.enableJavaScript` | bool | `false` | Enable JavaScript in the Percy rendering environment | +| `snapshotConfig.widths` | array | `null` | An array of integers representing the browser widths at which you want to take snapshots | +| `serializeConfig` | object | `{"enableJavaScript": true}` | Additional configuration to pass to the `PercyDOM.serialize` method injected into the web driver DOM | +| `snapshotServerTimeout` | int\ | `null` | [debug] The length of the time the Percy snapshot server will listen for incoming snapshots and send on to Percy.io (the amount of time needed to send all snapshots after a successful test suite run). No timeout is set by default | +| `throwOnAdapterError` | bool | `false` | [debug] Throw exception on adapter error | +| `cleanSnapshotStorage` | bool | `false` | [debug] Clean stored snapshot HTML after run | ## Running The Percy integration runs automatically with the test suite but will need your `PERCY_TOKEN` to be set to successfully send snapshots. For more information, see https://docs.percy.io/docs/environment-variables#section-required diff --git a/composer.json b/composer.json index feb6b8b..81618e6 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "codeception/module-webdriver": "^2.0", "eloquent/composer-npm-bridge": "^5.0", "ramsey/uuid": "^4.1", - "symfony/process": "^5.2" + "symfony/process": "^5.2", + "tr33m4n/codeception-module-percy-environment": "^1.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon index 880be82..a74dcca 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,6 @@ parameters: bootstrapFiles: - vendor/codeception/codeception/autoload.php ignoreErrors: - - '#Property Codeception\\Module\\Percy::\$webDriver#' - '#of function (.*) expects CurlHandle, resource given.#' - '#\$resource \(resource\|false\) does not accept CurlHandle.#' + - '#Method Codeception\\Module\\Percy\\ServiceContainer::(.*) should return (.*) but returns mixed.#' diff --git a/rector.php b/rector.php index 3f5ee71..5a56284 100644 --- a/rector.php +++ b/rector.php @@ -26,7 +26,8 @@ RemoveUselessParamTagRector::class, RemoveUselessReturnTagRector::class, ReturnTypeDeclarationRector::class => [ - __DIR__ . '/src/Codeception/Module/Percy/Exchange/Adapter/CurlAdapter.php' + __DIR__ . '/src/Codeception/Module/Percy/Exchange/Adapter/CurlAdapter.php', + __DIR__ . '/src/Codeception/Module/Percy/RequestManagement.php' ], TypedPropertyRector::class => [ __DIR__ . '/src/Codeception/Module/Percy/Exchange/Adapter/CurlAdapter.php' diff --git a/src/Codeception/Module/Percy.php b/src/Codeception/Module/Percy.php index f45f9b1..98bcc53 100644 --- a/src/Codeception/Module/Percy.php +++ b/src/Codeception/Module/Percy.php @@ -4,17 +4,18 @@ namespace Codeception\Module; +use Codeception\Lib\ModuleContainer; use Codeception\Module; -use Codeception\Module\Percy\ConfigProvider; -use Codeception\Module\Percy\Exception\ApplicationException; +use Codeception\Module\Percy\ConfigManagement; +use Codeception\Module\Percy\CreateSnapshot; use Codeception\Module\Percy\Exchange\Payload; -use Codeception\Module\Percy\FilepathResolver; -use Codeception\Module\Percy\InfoProvider; use Codeception\Module\Percy\ProcessManagement; use Codeception\Module\Percy\RequestManagement; +use Codeception\Module\Percy\ServiceContainer; use Codeception\TestInterface; use Exception; use Symfony\Component\Process\Exception\RuntimeException; +use tr33m4n\CodeceptionModulePercyEnvironment\EnvironmentProviderInterface; /** * Class Percy @@ -27,6 +28,8 @@ class Percy extends Module { public const NAMESPACE = 'Percy'; + public const PACKAGE_NAME = 'tr33m4n/codeception-module-percy'; + /** * @var array */ @@ -48,28 +51,45 @@ class Percy extends Module 'cleanSnapshotStorage' => false ]; - private ?WebDriver $webDriver = null; + private ConfigManagement $configManagement; + + private RequestManagement $requestManagement; + + private ProcessManagement $processManagement; - private ?string $percyCliJs = null; + private CreateSnapshot $createSnapshot; + + private EnvironmentProviderInterface $environmentProvider; + + private WebDriver $webDriver; /** - * {@inheritdoc} + * Percy constructor. * - * @throws \Exception + * @throws \Codeception\Exception\ModuleException + * @param array|null $config + * @param \Codeception\Lib\ModuleContainer $moduleContainer */ - public function _initialize(): void - { - /** @var array $moduleConfig */ - $moduleConfig = $this->_getConfig() ?? []; - ConfigProvider::set($moduleConfig); + public function __construct( + ModuleContainer $moduleContainer, + array $config = null + ) { + parent::__construct($moduleContainer, $config); + /** @var \Codeception\Module\WebDriver $webDriverModule */ $webDriverModule = $this->getModule('WebDriver'); - if (!$webDriverModule instanceof WebDriver) { - throw new ApplicationException('"WebDriver" module not found'); - } + /** @var array $percyModuleConfig */ + $percyModuleConfig = $this->_getConfig() ?? []; + + $serviceContainer = new ServiceContainer($webDriverModule, $percyModuleConfig); + + $this->configManagement = $serviceContainer->getConfigManagement(); + $this->requestManagement = $serviceContainer->getRequestManagement(); + $this->processManagement = $serviceContainer->getProcessManagement(); + $this->createSnapshot = $serviceContainer->getCreateSnapshot(); + $this->environmentProvider = $serviceContainer->getEnvironmentProvider(); $this->webDriver = $webDriverModule; - $this->percyCliJs = file_get_contents(FilepathResolver::percyCliBrowserJs()) ?: null; } /** @@ -77,6 +97,8 @@ public function _initialize(): void * * @throws \Codeception\Module\Percy\Exception\StorageException * @throws \JsonException + * @throws \tr33m4n\CodeceptionModulePercyEnvironment\Exception\EnvironmentException + * @throws \Codeception\Module\Percy\Exception\ConfigException * @param string $name * @param array $snapshotConfig */ @@ -84,42 +106,26 @@ public function takeAPercySnapshot( string $name, array $snapshotConfig = [] ): void { - // If we cannot access the CLI JS, return silently - if (!$this->percyCliJs) { - return; - } - - // If web driver has not been set, return - if (null === $this->webDriver) { - return; - } - - // If remote web driver has not been set, return + // If the remote web driver doesn't exist, return if (null === $this->webDriver->webDriver) { return; } // Add Percy CLI JS to page - $this->webDriver->executeJS($this->percyCliJs); - - /** @var array $moduleSnapshotConfig */ - $moduleSnapshotConfig = $this->_getConfig('snapshotConfig') ?? []; + $this->webDriver->executeJS($this->configManagement->getPercyCliBrowserJs()); /** @var string $domSnapshot */ $domSnapshot = $this->webDriver->executeJS( - sprintf( - 'return PercyDOM.serialize(%s)', - json_encode($this->_getConfig('serializeConfig'), JSON_THROW_ON_ERROR) - ) + sprintf('return PercyDOM.serialize(%s)', $this->configManagement->getSerializeConfig()) ); - RequestManagement::addPayload( - Payload::from(array_merge($moduleSnapshotConfig, $snapshotConfig)) + $this->requestManagement->addPayload( + Payload::from(array_merge($this->configManagement->getSnapshotConfig(), $snapshotConfig)) ->withName($name) ->withUrl($this->webDriver->webDriver->getCurrentURL()) - ->withDomSnapshot($domSnapshot) - ->withClientInfo(InfoProvider::getClientInfo()) - ->withEnvironmentInfo(InfoProvider::getEnvironmentInfo($this->webDriver->webDriver)) + ->withDomSnapshot($this->createSnapshot->execute($domSnapshot)) + ->withClientInfo($this->environmentProvider->getClientInfo()) + ->withEnvironmentInfo($this->environmentProvider->getEnvironmentInfo()) ); } @@ -132,14 +138,14 @@ public function takeAPercySnapshot( */ public function _afterSuite(): void { - if (!RequestManagement::hasPayloads()) { + if (!$this->requestManagement->hasPayloads()) { return; } $this->debugSection(self::NAMESPACE, 'Sending Percy snapshots..'); try { - RequestManagement::sendRequest(); + $this->requestManagement->sendRequest(); } catch (Exception $exception) { $this->debugConnectionError($exception); } @@ -158,7 +164,7 @@ public function _afterSuite(): void */ public function _failed(TestInterface $test, $fail): void { - RequestManagement::resetRequest(); + $this->requestManagement->resetRequest(); } /** @@ -174,16 +180,16 @@ private function debugConnectionError(Exception $exception): void [$exception->getMessage(), $exception->getTraceAsString()] ); - if (!$this->_getConfig('throwOnAdapterError')) { - return; - } - try { - ProcessManagement::stopPercySnapshotServer(); + $this->processManagement->stopPercySnapshotServer(); } catch (RuntimeException $exception) { // Fail silently if the process is not running } + if (!$this->configManagement->shouldThrowOnAdapterError()) { + return; + } + throw $exception; } } diff --git a/src/Codeception/Module/Percy/CleanSnapshots.php b/src/Codeception/Module/Percy/CleanSnapshots.php new file mode 100644 index 0000000..0ef9fef --- /dev/null +++ b/src/Codeception/Module/Percy/CleanSnapshots.php @@ -0,0 +1,35 @@ +configManagement = $configManagement; + } + + /** + * Clean snapshot directory + */ + public function execute(): void + { + if (!$this->configManagement->shouldCleanSnapshotStorage()) { + return; + } + + foreach (glob(codecept_output_dir(sprintf(CreateSnapshot::OUTPUT_FILE_PATTERN, '*'))) ?: [] as $snapshotFile) { + unlink($snapshotFile); + } + } +} diff --git a/src/Codeception/Module/Percy/ConfigManagement.php b/src/Codeception/Module/Percy/ConfigManagement.php new file mode 100644 index 0000000..a7d2176 --- /dev/null +++ b/src/Codeception/Module/Percy/ConfigManagement.php @@ -0,0 +1,193 @@ + + */ + private array $config; + + /** + * ConfigManagement constructor. + * + * @param array $config + */ + public function __construct( + array $config = [] + ) { + $this->config = $config; + } + + /** + * Get config + * + * @param string|null $key + * @return mixed|null + */ + public function get(string $key = null) + { + if (!$key) { + return $this->config; + } + + if (isset($this->config[$key])) { + return $this->config[$key]; + } + + return null; + } + + /** + * Get Percy CLI browser JS path + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return string + */ + public function getPercyCliBrowserJsPath(): string + { + return $this->validateFilePath(__DIR__ . '/../../../../node_modules/@percy/dom/dist/bundle.js'); + } + + /** + * Get Percy CLI executable path + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return string + */ + public function getPercyCliExecutablePath(): string + { + return $this->validateFilePath(__DIR__ . '/../../../../node_modules/.bin/percy'); + } + + /** + * Get Percy CLI browser JS + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return string + */ + public function getPercyCliBrowserJs(): string + { + $browserJs = file_get_contents($this->getPercyCliBrowserJsPath()); + if (!$browserJs) { + throw new ConfigException('Unable to resolve browser JS'); + } + + return $browserJs; + } + + /** + * Get snapshot base URL + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return string + */ + public function getSnapshotBaseUrl(): string + { + /** @var string $snapshotBaseUrl */ + $snapshotBaseUrl = $this->get('snapshotBaseUrl'); + if (!filter_var($snapshotBaseUrl, FILTER_VALIDATE_URL)) { + throw new ConfigException('Snapshot base URL is not a valid URL'); + } + + return $snapshotBaseUrl; + } + + /** + * Get snapshot path + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return string + */ + public function getSnapshotPath(): string + { + $snapshotPath = $this->get('snapshotPath'); + if (!is_string($snapshotPath)) { + throw new ConfigException('Snapshot path is not a string'); + } + + return $snapshotPath; + } + + /** + * Get snapshot server timeout + * + * @return float|null + */ + public function getSnapshotServerTimeout(): ?float + { + $snapshotServerTimeout = $this->get('snapshotServerTimeout'); + if (!is_numeric($snapshotServerTimeout)) { + return null; + } + + return (float) $snapshotServerTimeout; + } + + /** + * Get snapshot config + * + * @return array + */ + public function getSnapshotConfig(): array + { + $snapshotConfig = $this->get('snapshotConfig'); + if (!is_array($snapshotConfig)) { + return []; + } + + return $snapshotConfig; + } + + /** + * Get serialize config + * + * @throws \JsonException + * @return string + */ + public function getSerializeConfig(): string + { + return json_encode($this->get('serializeConfig'), JSON_THROW_ON_ERROR); + } + + /** + * Check if we should clean snapshot storage + * + * @return bool + */ + public function shouldCleanSnapshotStorage(): bool + { + return (bool) $this->get('cleanSnapshotStorage'); + } + + /** + * Check if we should throw on adapter error + * + * @return bool + */ + public function shouldThrowOnAdapterError(): bool + { + return (bool) $this->get('throwOnAdapterError'); + } + + /** + * Validate file path + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @param string $filePath + * @return string + */ + private function validateFilePath(string $filePath): string + { + if (!is_file($filePath)) { + throw new ConfigException(sprintf('File "%s" does not exist!', $filePath)); + } + + return $filePath; + } +} diff --git a/src/Codeception/Module/Percy/ConfigProvider.php b/src/Codeception/Module/Percy/ConfigProvider.php deleted file mode 100644 index 40e13ca..0000000 --- a/src/Codeception/Module/Percy/ConfigProvider.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - private static array $config = []; - - /** - * Set config - * - * @param array $config - */ - public static function set(array $config): void - { - self::$config = $config; - } - - /** - * Get config - * - * @param string|null $key - * @return mixed|null - */ - public static function get(string $key = null) - { - if (!$key) { - return self::$config; - } - - if (isset(self::$config[$key])) { - return self::$config[$key]; - } - - return null; - } -} diff --git a/src/Codeception/Module/Percy/SnapshotManagement.php b/src/Codeception/Module/Percy/CreateSnapshot.php similarity index 57% rename from src/Codeception/Module/Percy/SnapshotManagement.php rename to src/Codeception/Module/Percy/CreateSnapshot.php index 41d6e6e..6950c42 100644 --- a/src/Codeception/Module/Percy/SnapshotManagement.php +++ b/src/Codeception/Module/Percy/CreateSnapshot.php @@ -7,19 +7,19 @@ use Codeception\Module\Percy\Exception\StorageException; use Ramsey\Uuid\Uuid; -class SnapshotManagement +class CreateSnapshot { public const OUTPUT_FILE_PATTERN = 'dom_snapshots' . DIRECTORY_SEPARATOR . '%s.html'; /** - * Save DOM snapshot to file + * Create snapshot from DOM string * * @throws \Codeception\Module\Percy\Exception\StorageException * @throws \Exception * @param string $domString * @return \Codeception\Module\Percy\Snapshot */ - public static function save(string $domString): Snapshot + public function execute(string $domString): Snapshot { if (!function_exists('codecept_output_dir')) { throw new StorageException('`codecept_output_dir` function is not available!'); @@ -36,29 +36,11 @@ public static function save(string $domString): Snapshot chmod($fileDirectory, 0777); } - file_put_contents($filePath, $domString); - - return Snapshot::from($filePath); - } - - /** - * Load DOM snapshot from file - * - * @param \Codeception\Module\Percy\Snapshot $snapshot - * @return string - */ - public static function load(Snapshot $snapshot): string - { - return file_get_contents($snapshot->getFilePath()) ?: ''; - } - - /** - * Clean snapshot directory - */ - public static function clean(): void - { - foreach (glob(codecept_output_dir(sprintf(self::OUTPUT_FILE_PATTERN, '*'))) ?: [] as $snapshotFile) { - unlink($snapshotFile); + $writeResults = file_put_contents($filePath, $domString); + if (!$writeResults) { + throw new StorageException('Something went wrong when writing the DOM string'); } + + return Snapshot::create($filePath); } } diff --git a/src/Codeception/Module/Percy/Exception/ApplicationException.php b/src/Codeception/Module/Percy/Exception/ApplicationException.php deleted file mode 100644 index 82fc815..0000000 --- a/src/Codeception/Module/Percy/Exception/ApplicationException.php +++ /dev/null @@ -1,10 +0,0 @@ -setDefaults(); } - /** - * Create new instance - * - * @throws \Codeception\Module\Percy\Exception\AdapterException - * @param string $baseUrl - * @return \Codeception\Module\Percy\Exchange\Adapter\CurlAdapter - */ - public static function create(string $baseUrl): CurlAdapter - { - return new self($baseUrl); - } - /** * @inheritDoc */ diff --git a/src/Codeception/Module/Percy/Exchange/Client.php b/src/Codeception/Module/Percy/Exchange/Client.php index e2c993a..bed5d44 100644 --- a/src/Codeception/Module/Percy/Exchange/Client.php +++ b/src/Codeception/Module/Percy/Exchange/Client.php @@ -21,17 +21,6 @@ public function __construct( $this->adapter = $adapter; } - /** - * Create new instance - * - * @param \Codeception\Module\Percy\Exchange\Adapter\AdapterInterface $adapter - * @return \Codeception\Module\Percy\Exchange\Client - */ - public static function create(AdapterInterface $adapter): Client - { - return new self($adapter); - } - /** * @inheritDoc */ diff --git a/src/Codeception/Module/Percy/Exchange/ClientFactory.php b/src/Codeception/Module/Percy/Exchange/ClientFactory.php deleted file mode 100644 index ff30b7c..0000000 --- a/src/Codeception/Module/Percy/Exchange/ClientFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - */ @@ -51,7 +61,11 @@ public static function from(array $payloadArray): Payload return array_reduce( array_keys($payloadArray), static function (Payload $payload, string $configKey) use ($payloadArray): Payload { - ValidatePayloadKey::execute($configKey); + if (!in_array($configKey, self::PUBLIC_KEYS)) { + throw new InvalidArgumentException( + sprintf('"%s" cannot be set through config', $configKey) + ); + } return self::withValue($payload, $configKey, $payloadArray[$configKey]); }, @@ -81,17 +95,6 @@ public function withUrl(string $url): Payload return self::withValue(clone $this, self::URL, $url); } - /** - * With Percy CSS - * - * @param string|null $percyCss - * @return \Codeception\Module\Percy\Exchange\Payload - */ - public function withPercyCss(?string $percyCss): Payload - { - return self::withValue(clone $this, self::PERCY_CSS, $percyCss); - } - /** * With min height * @@ -106,13 +109,12 @@ public function withMinHeight(?int $minHeight): Payload /** * With DOM snapshot * - * @throws \Codeception\Module\Percy\Exception\StorageException - * @param string $domSnapshot + * @param \Codeception\Module\Percy\Snapshot $domSnapshot * @return \Codeception\Module\Percy\Exchange\Payload */ - public function withDomSnapshot(string $domSnapshot): Payload + public function withDomSnapshot(Snapshot $domSnapshot): Payload { - return self::withValue(clone $this, self::DOM_SNAPSHOT, SnapshotManagement::save($domSnapshot)); + return self::withValue(clone $this, self::DOM_SNAPSHOT, $domSnapshot); } /** @@ -126,17 +128,6 @@ public function withClientInfo(string $clientInfo): Payload return self::withValue(clone $this, self::CLIENT_INFO, $clientInfo); } - /** - * With enable JavaScript - * - * @param bool $enableJavaScript - * @return \Codeception\Module\Percy\Exchange\Payload - */ - public function withEnableJavaScript(bool $enableJavaScript): Payload - { - return self::withValue(clone $this, self::ENABLE_JAVASCRIPT, $enableJavaScript); - } - /** * With environment info * @@ -148,17 +139,6 @@ public function withEnvironmentInfo(string $environmentInfo): Payload return self::withValue(clone $this, self::ENVIRONMENT_INFO, $environmentInfo); } - /** - * With widths - * - * @param int[] $widths - * @return \Codeception\Module\Percy\Exchange\Payload - */ - public function withWidths(array $widths): Payload - { - return self::withValue(clone $this, self::WIDTHS, $widths); - } - /** * With value * @@ -193,24 +173,6 @@ public function getName(): string return $this->config[self::NAME]; } - /** - * Get DOM snapshot - * - * @return \Codeception\Module\Percy\Snapshot|null - */ - public function getDomSnapshot(): ?Snapshot - { - if (!array_key_exists(self::DOM_SNAPSHOT, $this->config)) { - return null; - } - - if (!$this->config[self::DOM_SNAPSHOT] instanceof Snapshot) { - return null; - } - - return $this->config[self::DOM_SNAPSHOT]; - } - /** * Encode config as JSON when casting to string * diff --git a/src/Codeception/Module/Percy/Exchange/ValidatePayloadKey.php b/src/Codeception/Module/Percy/Exchange/ValidatePayloadKey.php deleted file mode 100644 index 2d9234b..0000000 --- a/src/Codeception/Module/Percy/Exchange/ValidatePayloadKey.php +++ /dev/null @@ -1,34 +0,0 @@ -getCapabilities(); - - return self::$environmentInfo = sprintf( - 'codeception-php; %s; %s/%s', - $webDriverCapabilities->getPlatform(), - $webDriverCapabilities->getBrowserName(), - $webDriverCapabilities->getVersion() - ); - } - - /** - * Get client info - * - * @return string - */ - public static function getClientInfo(): string - { - if (null !== self::$clientInfo) { - return self::$clientInfo; - } - - return self::$clientInfo = sprintf( - '%s/%s', - ltrim(strstr(self::PACKAGE_NAME, '/') ?: '', '/'), - InstalledVersions::getVersion(self::PACKAGE_NAME) - ); - } -} diff --git a/src/Codeception/Module/Percy/ProcessManagement.php b/src/Codeception/Module/Percy/ProcessManagement.php index 7647bd4..d472856 100644 --- a/src/Codeception/Module/Percy/ProcessManagement.php +++ b/src/Codeception/Module/Percy/ProcessManagement.php @@ -11,25 +11,40 @@ class ProcessManagement { public const PERCY_NODE_PATH = 'PERCY_NODE_PATH'; - private static ?Process $process = null; + private ConfigManagement $configManagement; + + private ?Process $process = null; + + /** + * ProcessManagement constructor. + * + * @param \Codeception\Module\Percy\ConfigManagement $configManagement + */ + public function __construct( + ConfigManagement $configManagement + ) { + $this->configManagement = $configManagement; + } /** * Start Percy snapshot server * * @throws \Symfony\Component\Process\Exception\ProcessFailedException + * @throws \Codeception\Module\Percy\Exception\ConfigException */ - public static function startPercySnapshotServer(): void + public function startPercySnapshotServer(): void { - /** @var float|int|null $snapshotServerTimeout */ - $snapshotServerTimeout = ConfigProvider::get('snapshotServerTimeout') ?? null; + $this->process = new Process([ + self::resolveNodePath(), + $this->configManagement->getPercyCliExecutablePath(), + 'exec:start' + ]); - self::$process = new Process([self::resolveNodePath(), FilepathResolver::percyCliExecutable(), 'exec:start']); - self::$process->setTimeout($snapshotServerTimeout); - self::$process->start(); + $this->process->setTimeout($this->configManagement->getSnapshotServerTimeout()); + $this->process->start(); // Wait until server is ready - self::$process->waitUntil(static fn (string $type, string $output): bool => - strpos($output, 'Percy has started!') !== false); + $this->process->waitUntil(fn (string $type, string $output): bool => $this->hasServerStarted($output)); } /** @@ -37,13 +52,13 @@ public static function startPercySnapshotServer(): void * * @throws \Symfony\Component\Process\Exception\RuntimeException */ - public static function stopPercySnapshotServer(): void + public function stopPercySnapshotServer(): void { - if (!self::$process instanceof Process || !self::$process->isRunning()) { + if (!$this->process instanceof Process || !$this->process->isRunning()) { throw new RuntimeException('Percy snapshot server is not running'); } - self::$process->stop(); + $this->process->stop(); } /** @@ -52,8 +67,19 @@ public static function stopPercySnapshotServer(): void * * @return string */ - private static function resolveNodePath(): string + private function resolveNodePath(): string { return getenv(self::PERCY_NODE_PATH) ?: 'node'; } + + /** + * Determine whether the server has started + * + * @param string $cliOutput + * @return bool + */ + private function hasServerStarted(string $cliOutput): bool + { + return strpos($cliOutput, 'Percy has started!') !== false; + } } diff --git a/src/Codeception/Module/Percy/RequestManagement.php b/src/Codeception/Module/Percy/RequestManagement.php index f59148c..6396dfa 100644 --- a/src/Codeception/Module/Percy/RequestManagement.php +++ b/src/Codeception/Module/Percy/RequestManagement.php @@ -4,25 +4,55 @@ namespace Codeception\Module\Percy; -use Codeception\Module\Percy\Exception\ConfigException; -use Codeception\Module\Percy\Exchange\ClientFactory; +use Codeception\Module\Percy\Exchange\ClientInterface; use Codeception\Module\Percy\Exchange\Payload; class RequestManagement { + private ConfigManagement $configManagement; + + private CleanSnapshots $cleanSnapshots; + + private ProcessManagement $processManagement; + + private ClientInterface $client; + /** * @var \Codeception\Module\Percy\Exchange\Payload[] */ - private static array $payloads = []; + private array $payloads = []; + + /** + * RequestManagement constructor. + * + * @param \Codeception\Module\Percy\ConfigManagement $configManagement + * @param \Codeception\Module\Percy\CleanSnapshots $cleanSnapshots + * @param \Codeception\Module\Percy\ProcessManagement $processManagement + * @param \Codeception\Module\Percy\Exchange\ClientInterface $client + */ + public function __construct( + ConfigManagement $configManagement, + CleanSnapshots $cleanSnapshots, + ProcessManagement $processManagement, + ClientInterface $client + ) { + $this->configManagement = $configManagement; + $this->cleanSnapshots = $cleanSnapshots; + $this->processManagement = $processManagement; + $this->client = $client; + } /** * Add a payload * * @param \Codeception\Module\Percy\Exchange\Payload $payload + * @return \Codeception\Module\Percy\RequestManagement */ - public static function addPayload(Payload $payload): void + public function addPayload(Payload $payload): RequestManagement { - self::$payloads[] = $payload; + $this->payloads[] = $payload; + + return $this; } /** @@ -30,9 +60,9 @@ public static function addPayload(Payload $payload): void * * @return bool */ - public static function hasPayloads(): bool + public function hasPayloads(): bool { - return self::$payloads !== []; + return $this->payloads !== []; } /** @@ -41,40 +71,31 @@ public static function hasPayloads(): bool * @throws \Codeception\Module\Percy\Exception\AdapterException * @throws \Codeception\Module\Percy\Exception\ConfigException */ - public static function sendRequest(): void + public function sendRequest(): void { - if (!self::hasPayloads()) { + if (!$this->hasPayloads()) { return; } - $snapshotPath = ConfigProvider::get('snapshotPath'); - if (!is_string($snapshotPath)) { - throw new ConfigException('Snapshot path is not a string'); - } + $this->processManagement->startPercySnapshotServer(); - ProcessManagement::startPercySnapshotServer(); - $client = ClientFactory::create(); - - foreach (self::$payloads as $payload) { + foreach ($this->payloads as $payload) { codecept_debug(sprintf('[Percy] Sending snapshot "%s"', $payload->getName())); - $client->post($snapshotPath, $payload); + $this->client->post($this->configManagement->getSnapshotPath(), $payload); } - ProcessManagement::stopPercySnapshotServer(); + $this->processManagement->stopPercySnapshotServer(); - self::resetRequest(); + $this->resetRequest(); } /** * Reset payloads */ - public static function resetRequest(): void + public function resetRequest(): void { - self::$payloads = []; - - if (ConfigProvider::get('cleanSnapshotStorage')) { - SnapshotManagement::clean(); - } + $this->payloads = []; + $this->cleanSnapshots->execute(); } } diff --git a/src/Codeception/Module/Percy/ServiceContainer.php b/src/Codeception/Module/Percy/ServiceContainer.php new file mode 100644 index 0000000..4d73d3d --- /dev/null +++ b/src/Codeception/Module/Percy/ServiceContainer.php @@ -0,0 +1,287 @@ + + */ + private array $moduleConfig; + + /** + * @var array + */ + private array $services = []; + + /** + * ServiceContainer constructor. + * + * @param \Codeception\Module\WebDriver $webDriver + * @param array $moduleConfig + */ + public function __construct( + WebDriver $webDriver, + array $moduleConfig = [] + ) { + $this->serviceFactory = new ServiceFactory(); + $this->webDriver = $webDriver; + $this->moduleConfig = $moduleConfig; + } + + /** + * Get "env" helper + * + * @return \OndraM\CiDetector\Env + */ + public function getEnvHelper(): EnvHelper + { + return $this->resolveService(EnvHelper::class); + } + + /** + * Get event data provider + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\CiEnvironment\CiType\GitHub\EventDataProvider + */ + public function getEventDataProvider(): EventDataProvider + { + return $this->resolveService(EventDataProvider::class); + } + + /** + * Get Git API + * + * @return \CzProject\GitPhp\Git + */ + public function getGitApi(): Git + { + return $this->resolveService(Git::class); + } + + /** + * Get instantiated CI types + * + * @return array + */ + public function getCiTypes(): array + { + $ciTypes = [ + (string) CiType::APPVEYOR() => CiType\AppVeyor::class, + (string) CiType::AWS_CODEBUILD() => CiType\AwsCodeBuild::class, + (string) CiType::AZURE_PIPELINES() => CiType\AzurePipelines::class, + (string) CiType::BAMBOO() => CiType\Bamboo::class, + (string) CiType::BITBUCKET_PIPELINES() => CiType\BitbucketPipelines::class, + (string) CiType::BUDDY() => CiType\Buddy::class, + (string) CiType::CIRCLE() => CiType\Circle::class, + (string) CiType::CODESHIP() => CiType\CodeShip::class, + (string) CiType::CONTINUOUSPHP() => CiType\Continuousphp::class, + (string) CiType::DRONE() => CiType\Drone::class, + (string) CiType::GITHUB_ACTIONS() => CiType\GitHubActions::class, + (string) CiType::GITLAB() => CiType\GitLab::class, + (string) CiType::JENKINS() => CiType\Jenkins::class, + (string) CiType::SOURCEHUT() => CiType\SourceHut::class, + (string) CiType::TEAMCITY() => CiType\TeamCity::class, + (string) CiType::TRAVIS() => CiType\Travis::class, + (string) CiType::WERCKER() => CiType\Wercker::class, + ]; + + return array_map(function (string $ciTypeClass) { + if ($ciTypeClass === CiType\GitHubActions::class) { + return $this->resolveService($ciTypeClass, [$this->getEventDataProvider(), $this->getEnvHelper()]); + } + + return $this->resolveService($ciTypeClass, [$this->getEnvHelper()]); + }, $ciTypes); + } + + /** + * Get CI type pool + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\CiEnvironment\CiTypePool + */ + public function getCiTypePool(): CiTypePool + { + return $this->resolveService(CiTypePool::class, [$this->getCiTypes()]); + } + + /** + * Get CI type resolver + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\CiEnvironment\CiTypeResolver + */ + public function getCiTypeResolver(): CiTypeResolver + { + return $this->resolveService(CiTypeResolver::class, [$this->getCiTypePool(), $this->getEnvHelper()]); + } + + /** + * Get CI environment + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\CiEnvironment + */ + public function getCiEnvironment(): CiEnvironment + { + return $this->resolveService(CiEnvironment::class, [$this->getCiTypeResolver()]); + } + + /** + * Get Git environment + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\GitEnvironment + */ + public function getGitEnvironment(): GitEnvironment + { + return $this->resolveService(GitEnvironment::class, [$this->getGitApi(), codecept_root_dir()]); + } + + /** + * Get Percy environment + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\PercyEnvironment + */ + public function getPercyEnvironment(): PercyEnvironment + { + return $this->resolveService(PercyEnvironment::class); + } + + /** + * Get environment provider + * + * @return \tr33m4n\CodeceptionModulePercyEnvironment\EnvironmentProviderInterface + */ + public function getEnvironmentProvider(): EnvironmentProviderInterface + { + return $this->resolveService( + EnvironmentProvider::class, + [ + $this->getCiEnvironment(), + $this->getGitEnvironment(), + $this->getPercyEnvironment(), + $this->webDriver, + Percy::PACKAGE_NAME + ] + ); + } + + /** + * Get config management + * + * @return \Codeception\Module\Percy\ConfigManagement + */ + public function getConfigManagement(): ConfigManagement + { + return $this->resolveService(ConfigManagement::class, [$this->moduleConfig]); + } + + /** + * Get create snapshot + * + * @return \Codeception\Module\Percy\CreateSnapshot + */ + public function getCreateSnapshot(): CreateSnapshot + { + return $this->resolveService(CreateSnapshot::class); + } + + /** + * Get clean snapshots + * + * @return \Codeception\Module\Percy\CleanSnapshots + */ + public function getCleanSnapshots(): CleanSnapshots + { + return $this->resolveService(CleanSnapshots::class, [$this->getConfigManagement()]); + } + + /** + * Get process management + * + * @return \Codeception\Module\Percy\ProcessManagement + */ + public function getProcessManagement(): ProcessManagement + { + return $this->resolveService(ProcessManagement::class, [$this->getConfigManagement()]); + } + + /** + * Get adapter + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return \Codeception\Module\Percy\Exchange\Adapter\AdapterInterface + */ + public function getAdapter(): AdapterInterface + { + return $this->resolveService(CurlAdapter::class, [$this->getConfigManagement()->getSnapshotBaseUrl()]); + } + + /** + * Get client + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return \Codeception\Module\Percy\Exchange\ClientInterface + */ + public function getClient(): ClientInterface + { + return $this->resolveService(Client::class, [$this->getAdapter()]); + } + + /** + * Get request management + * + * @throws \Codeception\Module\Percy\Exception\ConfigException + * @return \Codeception\Module\Percy\RequestManagement + */ + public function getRequestManagement(): RequestManagement + { + return $this->resolveService( + RequestManagement::class, + [ + $this->getConfigManagement(), + $this->getCleanSnapshots(), + $this->getProcessManagement(), + $this->getClient() + ] + ); + } + + /** + * Resolve service + * + * @param class-string $className + * @param mixed[] $parameters + * @return mixed + */ + private function resolveService(string $className, array $parameters = []) + { + if (array_key_exists($className, $this->services)) { + return $this->services[$className]; + } + + return $this->services[$className] = $this->serviceFactory->create($className, $parameters); + } +} diff --git a/src/Codeception/Module/Percy/ServiceFactory.php b/src/Codeception/Module/Percy/ServiceFactory.php new file mode 100644 index 0000000..67364ef --- /dev/null +++ b/src/Codeception/Module/Percy/ServiceFactory.php @@ -0,0 +1,20 @@ + $parameters + * @return object + */ + public function create(string $className, array $parameters = []): object + { + return new $className(...$parameters); + } +} diff --git a/src/Codeception/Module/Percy/Snapshot.php b/src/Codeception/Module/Percy/Snapshot.php index 414a07f..2ec3dee 100644 --- a/src/Codeception/Module/Percy/Snapshot.php +++ b/src/Codeception/Module/Percy/Snapshot.php @@ -19,12 +19,12 @@ private function __construct() } /** - * From file path + * Create from file path * * @param string $filePath * @return \Codeception\Module\Percy\Snapshot */ - public static function from(string $filePath): Snapshot + public static function create(string $filePath): Snapshot { $snapshot = new self(); $snapshot->filePath = $filePath; @@ -47,6 +47,6 @@ public function getFilePath(): string */ public function jsonSerialize(): string { - return SnapshotManagement::load($this); + return file_get_contents($this->getFilePath()) ?: ''; } }