From 64d929ab03c1a25fe928d9f8d9ed79577d2a4b50 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Sat, 18 Jan 2025 10:47:32 +1100 Subject: [PATCH] Updated code formatting and tests. --- .github/workflows/test.yml | 2 +- README.md | 2 +- composer.json | 3 +- phpstan.neon | 4 +- .../ScreenshotContextInitializer.php | 32 +- .../Context/ScreenshotContext.php | 152 ++++---- .../BehatScreenshotExtension.php | 4 +- .../BehatScreenshotExtension/Tokenizer.php | 342 ++++++------------ tests/behat/bootstrap/BehatCliTrait.php | 4 + .../features/behatcli_screenshot.feature | 9 +- tests/phpunit/Traits/ReflectionTrait.php | 104 ++++++ tests/phpunit/Unit/ScreenshotContextTest.php | 23 +- .../phpunit/Unit/Tokenizer/TokenizerTest.php | 249 +++++-------- 13 files changed, 409 insertions(+), 521 deletions(-) create mode 100644 tests/phpunit/Traits/ReflectionTrait.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a79e6c4..9efe1a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: continue-on-error: ${{ vars.CI_LINT_IGNORE_FAILURE == '1' }} - name: Run unit tests - run: composer test-unit + run: composer test-coverage continue-on-error: ${{ vars.CI_TEST_UNIT_IGNORE_FAILURE == '1' }} - name: Run BDD tests diff --git a/README.md b/README.md index bf90d42..057daab 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ You may optionally specify size of browser window in the screenshot step: | Name | Default value | Description | |-------------------------|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | `dir` | `%paths.base%/screenshots` | Path to directory to save screenshots. Directory structure will be created if the directory does not exist. | -| `fail` | `true` | Capture screenshots for failed tests. | +| `fail` | `true` | Capture screenshot on test failure. | | `fail_prefix` | `failed_` | Prefix failed screenshots with `fail_` string. Useful to distinguish failed and intended screenshots. | | `purge` | `false` | Remove all files from the screenshots directory on each test run. Useful during debugging of tests. | | `filenamePattern` | `{datetime:u}.{feature_file}.feature_{step_line}.{ext}` | File name pattern for successful assertions. | diff --git a/composer.json b/composer.json index a884ccd..9a87b3c 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,8 @@ "phpcbf" ], "reset": "rm -Rf vendor vendor-bin composer.lock", + "test": "phpunit --no-coverage", "test-bdd": "behat --colors", - "test-unit": "phpunit" + "test-coverage": "phpunit" } } diff --git a/phpstan.neon b/phpstan.neon index 744d6ee..fdec3ed 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -21,7 +21,9 @@ parameters: # it is not possible to specify the type of the parameter, so we ignore # this error. message: '#.*no value type specified in iterable type array.#' - path: tests/phpunit/* + paths: + - tests/phpunit/* + - src/DrevOps/BehatScreenshotExtension/Tokenizer.php reportUnmatched: false - message: '#.*#' diff --git a/src/DrevOps/BehatScreenshotExtension/Context/Initializer/ScreenshotContextInitializer.php b/src/DrevOps/BehatScreenshotExtension/Context/Initializer/ScreenshotContextInitializer.php index 2092466..7febe7c 100644 --- a/src/DrevOps/BehatScreenshotExtension/Context/Initializer/ScreenshotContextInitializer.php +++ b/src/DrevOps/BehatScreenshotExtension/Context/Initializer/ScreenshotContextInitializer.php @@ -15,13 +15,18 @@ */ class ScreenshotContextInitializer implements ContextInitializer { + /** + * Flag to purge files in the directory. + */ + protected bool $needsPurging = TRUE; + /** * ScreenshotContextInitializer constructor. * * @param string $dir * Screenshot dir. * @param bool $fail - * Screenshot when fail. + * Create screenshot on test failure. * @param string $failPrefix * File name prefix for a failed test. * @param bool $purge @@ -31,13 +36,19 @@ class ScreenshotContextInitializer implements ContextInitializer { * @param string $filenamePatternFailed * File name pattern failed. * @param bool $showPath - * Show current path in screenshots. - * @param bool $needsPurging - * Check if need to actually purge. + * Show current URL in screenshots. * * @codeCoverageIgnore */ - public function __construct(protected string $dir, protected bool $fail, private readonly string $failPrefix, protected bool $purge, protected string $filenamePattern, protected string $filenamePatternFailed, protected bool $showPath = FALSE, protected bool $needsPurging = TRUE) { + public function __construct( + protected string $dir, + protected bool $fail, + private readonly string $failPrefix, + protected bool $purge, + protected string $filenamePattern, + protected string $filenamePatternFailed, + protected bool $showPath = FALSE, + ) { } /** @@ -46,11 +57,20 @@ public function __construct(protected string $dir, protected bool $fail, private public function initializeContext(Context $context): void { if ($context instanceof ScreenshotAwareContextInterface) { $dir = $this->resolveScreenshotDir(); - $context->setScreenshotParameters($dir, $this->fail, $this->failPrefix, $this->filenamePattern, $this->filenamePatternFailed, $this->showPath); + if ($this->shouldPurge() && $this->needsPurging) { $this->purgeFilesInDir($dir); $this->needsPurging = FALSE; } + + $context->setScreenshotParameters( + $dir, + $this->fail, + $this->failPrefix, + $this->filenamePattern, + $this->filenamePatternFailed, + $this->showPath + ); } } diff --git a/src/DrevOps/BehatScreenshotExtension/Context/ScreenshotContext.php b/src/DrevOps/BehatScreenshotExtension/Context/ScreenshotContext.php index a20a902..be36968 100644 --- a/src/DrevOps/BehatScreenshotExtension/Context/ScreenshotContext.php +++ b/src/DrevOps/BehatScreenshotExtension/Context/ScreenshotContext.php @@ -20,14 +20,14 @@ class ScreenshotContext extends RawMinkContext implements ScreenshotAwareContextInterface { /** - * Screenshot step line. + * Makes screenshot when fail. */ - protected string $stepLine; + protected bool $fail = FALSE; /** - * Makes screenshot when fail. + * Prefix for failed screenshot files. */ - protected bool $fail = FALSE; + protected string $failPrefix = ''; /** * Screenshot directory name. @@ -35,9 +35,14 @@ class ScreenshotContext extends RawMinkContext implements ScreenshotAwareContext protected string $dir = ''; /** - * Prefix for failed screenshot files. + * Filename pattern. */ - protected string $failPrefix = ''; + protected string $filenamePattern; + + /** + * Filename pattern failed. + */ + protected string $filenamePatternFailed; /** * Show the path in the screenshot. @@ -45,7 +50,7 @@ class ScreenshotContext extends RawMinkContext implements ScreenshotAwareContext protected bool $showPath = FALSE; /** - * Debug information to be outputted in screenshot. + * Debug information to be added to a screenshot. * * @var array */ @@ -56,16 +61,6 @@ class ScreenshotContext extends RawMinkContext implements ScreenshotAwareContext */ protected BeforeStepScope $beforeStepScope; - /** - * Filename pattern. - */ - protected string $filenamePattern; - - /** - * Filename pattern failed. - */ - protected string $filenamePatternFailed; - /** * {@inheritdoc} */ @@ -81,7 +76,7 @@ public function setScreenshotParameters(string $dir, bool $fail, string $failPre } /** - * Init values required for snapshots. + * Init values required for screenshots. * * @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope * Scenario scope. @@ -114,20 +109,16 @@ public function beforeScenarioInit(BeforeScenarioScope $scope): void { } /** - * Init values required for snapshot. + * Init values required for a screenshot. * * @BeforeStep */ public function beforeStepInit(BeforeStepScope $scope): void { - $featureFile = $scope->getFeature()->getFile(); - if (!$featureFile) { - throw new \RuntimeException('Feature file not found.'); - } $this->beforeStepScope = $scope; } /** - * After scope event handler to print last response on error. + * After step handler to print last response on error. * * @param \Behat\Behat\Hook\Scope\AfterStepScope $event * After scope event. @@ -138,17 +129,15 @@ public function beforeStepInit(BeforeStepScope $scope): void { * @AfterStep */ public function printLastResponseOnError(AfterStepScope $event): void { - if ($this->fail && !$event->getTestResult()->isPassed()) { - $this->iSaveScreenshot(TRUE, NULL); + if (!$event->getTestResult()->isPassed() && $this->fail) { + $this->iSaveScreenshot(TRUE); } } /** - * Save debug screenshot. - * - * Handles different driver types. + * Save screenshot content into a file. * - * @param bool $fail + * @param bool $is_failure * Denotes if this was called in a context of the failed * test. * @param string|null $filename @@ -160,29 +149,34 @@ public function printLastResponseOnError(AfterStepScope $event): void { * @When save screenshot * @When I save screenshot */ - public function iSaveScreenshot(bool $fail = FALSE, ?string $filename = NULL): void { - $fileName = $this->makeFileName('html', $filename, $fail); + public function iSaveScreenshot(bool $is_failure = FALSE, ?string $filename = NULL): void { + $file_name = $this->makeFileName('html', $filename, $is_failure); + try { - $data = $this->getResponseHtml(); + $driver = $this->getSession()->getDriver(); + $content = $driver->getContent(); + + $info = $this->renderDebugInformation(); + $content = empty($info) ? $content : $info . '
' . $content; } catch (DriverException) { - // Do not do anything if the driver does not have any content - most + // Do nothing if the driver does not have any content - most // likely the page has not been loaded yet. return; } - $this->saveScreenshotData($fileName, $data); + $this->saveScreenshotContent($file_name, $content); // Drivers that do not support making screenshots, including Goutte // driver that is shipped with Behat, throw exception. For such drivers, // screenshot stored as an HTML page (without referenced assets). try { $driver = $this->getSession()->getDriver(); - $data = $driver->getScreenshot(); + $content = $driver->getScreenshot(); // Preserve filename, but change the extension - this is to group // content and screenshot files together by name. - $fileName = $this->makeFileName('png', $filename, $fail); - $this->saveScreenshotData($fileName, $data); + $file_name = $this->makeFileName('png', $filename, $is_failure); + $this->saveScreenshotContent($file_name, $content); } // @codeCoverageIgnoreStart catch (UnsupportedDriverActionException) { @@ -223,11 +217,12 @@ public function iSaveScreenshotWithName(string $filename): void { */ public function iSaveSizedScreenshot(string|int $width = 1440, string|int $height = 900): void { try { - $this->getSession()->resizeWindow((int) $width, (int) $height, 'current'); + $this->getSession()->resizeWindow(intval($width), intval($height), 'current'); } catch (UnsupportedDriverActionException) { // Nothing to do here - drivers without resize support may proceed. } + $this->iSaveScreenshot(); } @@ -242,24 +237,28 @@ public function getBeforeStepScope(): BeforeStepScope { } /** - * Get current timestamp. - * - * @return int - * Current timestamp. + * Adds debug information to context. * - * @codeCoverageIgnore + * @param string $label + * Debug information label. + * @param string $value + * Debug information value. */ - public function getCurrentTime(): int { - return time(); + public function appendDebugInformation(string $label, string $value): void { + $this->debugInformation[$label] = $value; } /** - * Gets the debug information for screenshot. + * Render debug information. * * @return string - * Information to prepend to screenshot + * Rendered debug information. */ - protected function getDebugInformation(): string { + public function renderDebugInformation(): string { + if ($this->showPath) { + $this->appendDebugInformation('Current URL', $this->getSession()->getCurrentUrl()); + } + return implode("\n", array_map( fn($key, $value): string => sprintf('%s: %s', $key, $value), array_keys($this->debugInformation), @@ -268,47 +267,31 @@ protected function getDebugInformation(): string { } /** - * Gets last response content with any debug information. - * - * @return string - * Response content with debug information. + * Get current timestamp. * - * @throws \Behat\Mink\Exception\DriverException - * @throws \Behat\Mink\Exception\UnsupportedDriverActionException - */ - protected function getResponseHtml(): string { - if ($this->showPath) { - $this->addDebugInformation('Current path', $this->getSession()->getCurrentUrl()); - } - - $driver = $this->getSession()->getDriver(); - - return $this->getDebugInformation() . $driver->getContent(); - } - - /** - * Adds debug information to context. + * @return int + * Current timestamp. * - * @param string $label - * Debug information label. - * @param string $value - * Debug information value. + * @codeCoverageIgnore */ - public function addDebugInformation(string $label, string $value): void { - $this->debugInformation[$label] = $value; + public function getCurrentTime(): int { + return time(); } /** - * Save screenshot data into a file. + * Save screenshot content into a file. * * @param string $filename * File name to write. - * @param string $data - * Data to write into a file. + * @param string $content + * Content to write into a file. */ - protected function saveScreenshotData(string $filename, string $data): void { + protected function saveScreenshotContent(string $filename, string $content): void { $this->prepareDir($this->dir); - file_put_contents($this->dir . DIRECTORY_SEPARATOR . $filename, $data); + $success = file_put_contents($this->dir . DIRECTORY_SEPARATOR . $filename, $content); + if ($success === FALSE) { + throw new \RuntimeException(sprintf('Failed to save screenshot to %s', $filename)); + } } /** @@ -318,8 +301,7 @@ protected function saveScreenshotData(string $filename, string $data): void { * Name of preparing directory. */ protected function prepareDir(string $dir): void { - $fs = new Filesystem(); - $fs->mkdir($dir, 0755); + (new Filesystem())->mkdir($dir, 0755); } /** @@ -331,7 +313,7 @@ protected function prepareDir(string $dir): void { * File extension without dot. * @param string|null $filename * Optional file name. - * @param bool $fail + * @param bool $is_failure * Make filename for fail case. * * @return string @@ -339,8 +321,8 @@ protected function prepareDir(string $dir): void { * * @throws \Exception */ - protected function makeFileName(string $ext, ?string $filename = NULL, bool $fail = FALSE): string { - if ($fail) { + protected function makeFileName(string $ext, ?string $filename = NULL, bool $is_failure = FALSE): string { + if ($is_failure) { $filename = $this->filenamePatternFailed; } elseif (empty($filename)) { @@ -377,7 +359,7 @@ protected function makeFileName(string $ext, ?string $filename = NULL, bool $fai 'step_line' => $step->getLine(), 'feature_file' => $feature->getFile(), 'url' => $url, - 'time' => $this->getCurrentTime(), + 'timestamp' => $this->getCurrentTime(), 'fail_prefix' => $this->failPrefix, ]; diff --git a/src/DrevOps/BehatScreenshotExtension/ServiceContainer/BehatScreenshotExtension.php b/src/DrevOps/BehatScreenshotExtension/ServiceContainer/BehatScreenshotExtension.php index 0cd003b..74dfbce 100644 --- a/src/DrevOps/BehatScreenshotExtension/ServiceContainer/BehatScreenshotExtension.php +++ b/src/DrevOps/BehatScreenshotExtension/ServiceContainer/BehatScreenshotExtension.php @@ -78,8 +78,8 @@ public function configure(ArrayNodeDefinition $builder): void { ->defaultValue('{datetime:U}.{fail_prefix}{feature_file}.feature_{step_line}.{ext}') ->end() ->scalarNode('show_path') - ->cannotBeEmpty() - ->defaultValue(FALSE) + ->cannotBeEmpty() + ->defaultValue(FALSE) ->end(); // @formatter:on // @phpcs:enable Drupal.WhiteSpace.ObjectOperatorIndent.Indent diff --git a/src/DrevOps/BehatScreenshotExtension/Tokenizer.php b/src/DrevOps/BehatScreenshotExtension/Tokenizer.php index 58c0231..0ec6127 100644 --- a/src/DrevOps/BehatScreenshotExtension/Tokenizer.php +++ b/src/DrevOps/BehatScreenshotExtension/Tokenizer.php @@ -5,7 +5,7 @@ namespace DrevOps\BehatScreenshotExtension; /** - * Handler token replacements. + * Handles token replacements. */ class Tokenizer { @@ -23,29 +23,26 @@ class Tokenizer { * @throws \Exception */ public static function replaceTokens(string $text, array $data = []): string { - $replacement = $text; $tokens = self::scanTokens($text); - $tokenReplacements = self::extractTokens($tokens, $data); - - if (!empty($tokenReplacements)) { - // If token replacements have {step_name} token. - // We need move {step_name} token to the last position, - // because {step_name} token may contain other tokens. - foreach ($tokenReplacements as $token => $replacement) { - if ('{step_name}' === $token) { - $element = [$token => $replacement]; - unset($tokenReplacements[$token]); - break; - } - } - if (isset($element)) { - $tokenReplacements = array_merge($tokenReplacements, $element); - } - $replacement = strtr($text, $tokenReplacements); + if (empty($tokens)) { + return $text; } - return $replacement; + $replacements = self::extractTokens($tokens, $data); + + // Move {step_name} token to the last position as it may contain other + // tokens. + foreach ($replacements as $name => $value) { + if ($name === '{step_name}') { + $replaced = [$name => $value]; + unset($replacements[$name]); + $replacements = array_merge($replacements, $replaced); + break; + } + } + + return strtr($text, $replacements); } /** @@ -58,47 +55,53 @@ public static function replaceTokens(string $text, array $data = []): string { * The tokens keyed by the token name. */ public static function scanTokens(string $text): array { - $pattern = '/\{(.*?)\}/'; + $pattern = '/\{(.*?)}/'; + preg_match_all($pattern, $text, $matches); + $tokens = []; - foreach ($matches[0] as $key => $match) { - $tokens[$match] = $matches[1][$key]; + foreach ($matches[0] as $key => $name) { + $tokens[$name] = $matches[1][$key]; } return $tokens; } /** - * Replace {feature} token. + * Build replacements tokens. * - * @param string $token - * Original token. - * @param string $name - * Token name. - * @param string|null $qualifier - * Token qualifier. - * @param string|null $format - * Token format. + * @param string[] $tokens + * Token. * @param array $data * Extra data to provide context to replace token. * - * @return string - * Token replacement. + * @return array + * Replacements has key as token and value as token replacement. */ - public static function replaceFeatureToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { - $replacement = $token; - if (isset($data['feature_file']) && is_string($data['feature_file'])) { - $featureFile = $data['feature_file']; - if (!empty($featureFile)) { - $replacement = basename($featureFile, '.feature'); + protected static function extractTokens(array $tokens, array $data): array { + $replacements = []; + + foreach ($tokens as $original_token => $token) { + $parts = explode(':', $token); + + $qualifier = NULL; + $format = $parts[1] ?? NULL; + + $name_qualifier = $parts[0]; + $name_qualifier_parts = explode('_', $name_qualifier); + $name = array_shift($name_qualifier_parts); + if (!empty($name_qualifier_parts)) { + $qualifier = implode('_', $name_qualifier_parts); } + + $replacements[$original_token] = self::buildTokenReplacement($original_token, $name, $qualifier, $format, $data); } - return $replacement; + return $replacements; } /** - * Replace {ext} token. + * Build replacement for a token. * * @param string $token * Original token. @@ -114,149 +117,112 @@ public static function replaceFeatureToken(string $token, string $name, ?string * @return string * Token replacement. */ - public static function replaceExtToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { - $ext = 'html'; - if (isset($data['ext']) && is_string($data['ext']) && $data['ext'] !== '') { - $ext = $data['ext']; + protected static function buildTokenReplacement(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + $method = 'replace' . ucfirst($name) . 'Token'; + + if (is_callable([self::class, $method])) { + $token = self::$method($token, $name, $qualifier, $format, $data); } - return $ext; + return $token; + } + + /** + * Replace {feature} token. + */ + protected static function replaceFeatureToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + return !empty($data['feature_file']) && is_string($data['feature_file']) ? basename($data['feature_file'], '.feature') : $token; + } + + /** + * Replace {ext} token. + */ + protected static function replaceExtToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + return isset($data['ext']) && is_string($data['ext']) && $data['ext'] !== '' ? $data['ext'] : 'html'; } /** * Replace {step} token. - * - * @param string $token - * Original token. - * @param string $name - * Token name. - * @param string|null $qualifier - * Token qualifier. - * @param string|null $format - * Token format. - * @param array $data - * Extra data to provide context to replace token. - * - * @return string - * Token replacement. */ - public static function replaceStepToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { - $replacement = $token; - switch ($qualifier) { - case 'line': - if (isset($data['step_line']) && (is_string($data['step_line']) || is_int($data['step_line']))) { - $replacement = (string) $data['step_line']; - if ($format) { - $replacement = sprintf($format, $replacement); - } - } - break; + protected static function replaceStepToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + if ($qualifier == 'line' && isset($data['step_line']) && (is_string($data['step_line']) || is_int($data['step_line']))) { + return $format ? sprintf($format, intval($data['step_line'])) : strval($data['step_line']); + } - case 'name': - default: - if (isset($data['step_name']) && is_string($data['step_name'])) { - $stepName = $data['step_name']; - $replacement = str_replace([' ', '"'], ['_', ''], $stepName); - } - break; + if (isset($data['step_name']) && is_string($data['step_name'])) { + return str_replace([' ', '"'], ['_', ''], $data['step_name']); } - return $replacement; + return $token; } /** * Replace {datetime} token. - * - * @param string $token - * Original token. - * @param string $name - * Token name. - * @param string|null $qualifier - * Token qualifier. - * @param string|null $format - * Token format. - * @param array $data - * Extra data to provide context to replace token. - * - * @return string - * Token replacement. - * - * @throws \Exception */ - public static function replaceDatetimeToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + protected static function replaceDatetimeToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { $timestamp = NULL; - if ($data['time']) { - if (!is_int($data['time'])) { - throw new \Exception('Time must be an integer.'); + + if (isset($data['timestamp'])) { + if (!is_scalar($data['timestamp'])) { + throw new \InvalidArgumentException('Timestamp must be numeric.'); } - $timestamp = $data['time']; - } - if ($format) { - return date($format, $timestamp); + $timestamp = intval($data['timestamp']); + + if ($timestamp < 1) { + throw new \InvalidArgumentException('Timestamp must be greater than 0.'); + } } - return date('Ymd_His', $timestamp); + return $timestamp ? date($format ?: 'Ymd_His', $timestamp) : $token; } /** * Replace {url} token. - * - * @param string $token - * Original token. - * @param string $name - * Token name. - * @param string|null $qualifier - * Token qualifier. - * @param string|null $format - * Token format. - * @param array $data - * Extra data to provide context to replace token. - * - * @return string - * Token replacement. - * - * @throws \Exception */ - public static function replaceUrlToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + protected static function replaceUrlToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { $replacement = $token; + if (isset($data['url']) && is_string($data['url'])) { $url = $data['url']; - $urlParts = parse_url($url); - if (!$urlParts) { - throw new \Exception('Could not parse url.'); + $url_parts = parse_url($url); + + if (!$url_parts) { + throw new \RuntimeException(sprintf('Could not parse URL "%s".', $url)); } + switch ($qualifier) { case 'origin': - $replacement = sprintf('%s://%s', $urlParts['scheme'], $urlParts['host']); + $replacement = sprintf('%s://%s', $url_parts['scheme'], $url_parts['host']); break; case 'relative': - $replacement = trim($urlParts['path'], '/'); - $replacement = (isset($urlParts['query'])) ? $replacement . '?' . $urlParts['query'] : $replacement; - $replacement = (isset($urlParts['fragment'])) ? $replacement . '#' . $urlParts['fragment'] : $replacement; + $replacement = trim($url_parts['path'], '/'); + $replacement = isset($url_parts['query']) ? $replacement . '?' . $url_parts['query'] : $replacement; + $replacement = isset($url_parts['fragment']) ? $replacement . '#' . $url_parts['fragment'] : $replacement; break; case 'domain': - $replacement = $urlParts['host']; + $replacement = $url_parts['host']; break; case 'path': - $replacement = trim($urlParts['path'], '/'); + $replacement = trim($url_parts['path'], '/'); break; case 'query': - $replacement = (isset($urlParts['query'])) ? $urlParts['query'] : ''; + $replacement = $url_parts['query'] ?: ''; break; case 'fragment': - $replacement = (isset($urlParts['fragment'])) ? $urlParts['fragment'] : ''; + $replacement = $url_parts['fragment'] ?: ''; break; default: $replacement = $url; break; } + $replacement = urlencode($replacement); } @@ -265,115 +231,9 @@ public static function replaceUrlToken(string $token, string $name, ?string $qua /** * Replace {fail} token. - * - * @param string $token - * Original token. - * @param string $name - * Token name. - * @param string|null $qualifier - * Token qualifier. - * @param string|null $format - * Token format. - * @param array $data - * Extra data to provide context to replace token. - * - * @return string - * Token replacement. */ - public static function replaceFailToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { - $replacement = $token; - if (!empty($data['fail_prefix']) && is_string($data['fail_prefix'])) { - $replacement = $data['fail_prefix']; - } - - return $replacement; - } - - /** - * Build replacements tokens. - * - * @param string[] $tokens - * Token. - * @param array $data - * Extra data to provide context to replace token. - * - * @return array - * Replacements has key as token and value as token replacement. - * - * @throws \Exception - */ - public static function extractTokens(array $tokens, array $data): array { - $replacements = []; - foreach ($tokens as $originalToken => $token) { - $tokenParts = explode(':', $token); - $qualifier = NULL; - $format = NULL; - $nameQualifier = $tokenParts[0]; - if (isset($tokenParts[1])) { - $format = $tokenParts[1]; - } - $nameQualifierParts = explode('_', $nameQualifier); - $name = array_shift($nameQualifierParts); - if (!empty($nameQualifierParts)) { - $qualifier = implode('_', $nameQualifierParts); - } - $replacements[$originalToken] = self::buildTokenReplacement($originalToken, $name, $qualifier, $format, $data); - } - - return $replacements; - } - - /** - * Build replacement for a token. - * - * @param string $token - * Original token. - * @param string $name - * Token name. - * @param string|null $qualifier - * Token qualifier. - * @param string|null $format - * Token format. - * @param array $data - * Extra data to provide context to replace token. - * - * @return string - * Token replacement. - * - * @throws \Exception - */ - public static function buildTokenReplacement(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { - $replacement = $token; - switch ($name) { - case 'feature': - $replacement = self::replaceFeatureToken($token, $name, $qualifier, $format, $data); - break; - - case 'url': - $replacement = self::replaceUrlToken($token, $name, $qualifier, $format, $data); - break; - - case 'datetime': - $replacement = self::replaceDatetimeToken($token, $name, $qualifier, $format, $data); - break; - - case 'fail': - $replacement = self::replaceFailToken($token, $name, $qualifier, $format, $data); - break; - - case 'ext': - $replacement = self::replaceExtToken($token, $name, $qualifier, $format, $data); - break; - - case 'step': - $replacement = self::replaceStepToken($token, $name, $qualifier, $format, $data); - break; - - default: - break; - } - - return $replacement; + protected static function replaceFailToken(string $token, string $name, ?string $qualifier = NULL, ?string $format = NULL, array $data = []): string { + return !empty($data['fail_prefix']) && is_string($data['fail_prefix']) ? $data['fail_prefix'] : $token; } } diff --git a/tests/behat/bootstrap/BehatCliTrait.php b/tests/behat/bootstrap/BehatCliTrait.php index 50a735b..d68bc50 100644 --- a/tests/behat/bootstrap/BehatCliTrait.php +++ b/tests/behat/bootstrap/BehatCliTrait.php @@ -332,9 +332,11 @@ public function behatCliAssertFileShouldExist($wildcard): void { public function behatCliAssertFileShouldContain($wildcard, PyStringNode $text): void { $wildcard = $this->workingDir . DIRECTORY_SEPARATOR . $wildcard; $matches = glob($wildcard); + if (empty($matches)) { throw new \Exception(sprintf("Unable to find screenshot file matching wildcard '%s'.", $wildcard)); } + $path = $matches[0]; $file_content = trim(file_get_contents($path)); @@ -358,10 +360,12 @@ public function behatCliAssertFileShouldContain($wildcard, PyStringNode $text): */ public function behatCliAssertFileNotShouldContain($wildcard, PyStringNode $text): void { $wildcard = $this->workingDir . DIRECTORY_SEPARATOR . $wildcard; + $matches = glob($wildcard); if (empty($matches)) { throw new \Exception(sprintf("Unable to find screenshot file matching wildcard '%s'.", $wildcard)); } + $path = $matches[0]; $file_content = trim(file_get_contents($path)); diff --git a/tests/behat/features/behatcli_screenshot.feature b/tests/behat/features/behatcli_screenshot.feature index 32c7dae..1c26307 100644 --- a/tests/behat/features/behatcli_screenshot.feature +++ b/tests/behat/features/behatcli_screenshot.feature @@ -261,7 +261,8 @@ Feature: Screenshot context # Assert that the file from the previous run is not present. And behat cli file wildcard "screenshots_custom/*.failed_stub.feature_6\.html" should not exist - Scenario: Test Screenshot context with 'show_path' set to 'true' will output current path to screenshot files. + @wip1 + Scenario: Test Screenshot context with 'show_path' set to 'true' will output current URL to screenshot files. Given screenshot context behat configuration with value: """ DrevOps\BehatScreenshotExtension: @@ -277,10 +278,10 @@ Feature: Screenshot context Then it should fail And behat screenshot file matching "screenshots/*.failed_stub.feature_6\.html" should contain: """ - Current path: http://0.0.0.0:8888/screenshot.html + Current URL: http://0.0.0.0:8888/screenshot.html """ - Scenario: Test Screenshot context with 'show_path' set to 'false' will not output current path to screenshot files. + Scenario: Test Screenshot context with 'show_path' set to 'false' will not output current URL to screenshot files. Given screenshot context behat configuration with value: """ DrevOps\BehatScreenshotExtension: @@ -296,5 +297,5 @@ Feature: Screenshot context Then it should fail And behat screenshot file matching "screenshots/*.failed_stub.feature_6\.html" should not contain: """ - Current path: http://0.0.0.0:8888/screenshot.html + Current URL: http://0.0.0.0:8888/screenshot.html """ diff --git a/tests/phpunit/Traits/ReflectionTrait.php b/tests/phpunit/Traits/ReflectionTrait.php new file mode 100644 index 0000000..a686247 --- /dev/null +++ b/tests/phpunit/Traits/ReflectionTrait.php @@ -0,0 +1,104 @@ +hasMethod($name)) { + throw new \InvalidArgumentException(sprintf('Method %s does not exist', $name)); + } + + $method = $class->getMethod($name); + + $original_accessibility = $method->isPublic(); + + // Set method accessibility to true, so it can be invoked. + $method->setAccessible(TRUE); + + // If the method is static, we won't pass an object instance to invokeArgs() + // Otherwise, we ensure to pass the object instance. + $invoke_object = $method->isStatic() ? NULL : (is_object($object) ? $object : NULL); + + // Ensure we have an object for non-static methods. + if (!$method->isStatic() && $invoke_object === NULL) { + throw new \InvalidArgumentException("An object instance is required for non-static methods"); + } + + $result = $method->invokeArgs($invoke_object, $args); + + // Reset the method's accessibility to its original state. + $method->setAccessible($original_accessibility); + + return $result; + } + + /** + * Set protected property value. + * + * @param object $object + * Object to set the value on. + * @param string $property + * Property name to set the value. Property should exists in the object. + * @param mixed $value + * Value to set to the property. + */ + protected static function setProtectedValue($object, $property, mixed $value): void { + $class = new \ReflectionClass($object::class); + $property = $class->getProperty($property); + $property->setAccessible(TRUE); + + $property->setValue($object, $value); + } + + /** + * Get protected value from the object. + * + * @param object $object + * Object to set the value on. + * @param string $property + * Property name to get the value. Property should exists in the object. + * + * @return mixed + * Protected property value. + */ + protected static function getProtectedValue($object, $property): mixed { + $class = new \ReflectionClass($object::class); + $property = $class->getProperty($property); + $property->setAccessible(TRUE); + + return $property->getValue($class); + } + +} diff --git a/tests/phpunit/Unit/ScreenshotContextTest.php b/tests/phpunit/Unit/ScreenshotContextTest.php index 401e44e..f453df1 100644 --- a/tests/phpunit/Unit/ScreenshotContextTest.php +++ b/tests/phpunit/Unit/ScreenshotContextTest.php @@ -59,19 +59,6 @@ public function testBeforeStepInit(): void { $this->assertEquals($scope, $screenshot_context->getBeforeStepScope()); } - public function testBeforeStepInitThrowError(): void { - $env = $this->createMock(Environment::class); - $feature_node = $this->createMock(FeatureNode::class); - $step_node = $this->createMock(StepNode::class); - - $feature_node->method('getFile')->willReturn(FALSE); - $this->expectException(\RuntimeException::class); - - $screenshot_context = new ScreenshotContext(); - $scope = new BeforeStepScope($env, $feature_node, $step_node); - $screenshot_context->beforeStepInit($scope); - } - public function testPrintLastResponseOnError(): void { $env = $this->createMock(Environment::class); $feature_node = $this->createMock(FeatureNode::class); @@ -122,7 +109,7 @@ public function testIsaveScreenshot(): void { $screenshot_context = $this->createPartialMock(ScreenshotContext::class, [ 'getSession', 'makeFileName', - 'saveScreenshotData', + 'saveScreenshotContent', ]); $session = $this->createMock(Session::class); $driver = $this->createMock(Selenium2Driver::class); @@ -132,7 +119,7 @@ public function testIsaveScreenshot(): void { $screenshot_context->method('getSession')->willReturn($session); $screenshot_context->method('makeFileName')->willReturn('test-file-name'); - $screenshot_context->expects($this->exactly(2))->method('saveScreenshotData'); + $screenshot_context->expects($this->exactly(2))->method('saveScreenshotContent'); $screenshot_context->iSaveScreenshot(); } @@ -140,7 +127,7 @@ public function testIsaveScreenshotThrowException(): void { $screenshot_context = $this->createPartialMock(ScreenshotContext::class, [ 'getSession', 'makeFileName', - 'saveScreenshotData', + 'saveScreenshotContent', ]); $session = $this->createMock(Session::class); $driver = $this->createMock(Selenium2Driver::class); @@ -150,7 +137,7 @@ public function testIsaveScreenshotThrowException(): void { $screenshot_context->method('getSession')->willReturn($session); $screenshot_context->method('makeFileName')->willReturn('test-file-name'); - $screenshot_context->expects($this->never())->method('saveScreenshotData'); + $screenshot_context->expects($this->never())->method('saveScreenshotContent'); $screenshot_context->iSaveScreenshot(); } @@ -166,7 +153,7 @@ public function testSaveScreenshotData(string $filename, string $data): void { TRUE, ); $screenshot_context_reflection = new \ReflectionClass($screenshot_context); - $method = $screenshot_context_reflection->getMethod('saveScreenshotData'); + $method = $screenshot_context_reflection->getMethod('saveScreenshotContent'); $method->setAccessible(TRUE); $method->invokeArgs($screenshot_context, [$filename, $data]); $filepath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename; diff --git a/tests/phpunit/Unit/Tokenizer/TokenizerTest.php b/tests/phpunit/Unit/Tokenizer/TokenizerTest.php index 72275ae..8a7add2 100644 --- a/tests/phpunit/Unit/Tokenizer/TokenizerTest.php +++ b/tests/phpunit/Unit/Tokenizer/TokenizerTest.php @@ -4,6 +4,7 @@ namespace DrevOps\BehatScreenshot\Tests\Unit\Tokenizer; +use DrevOps\BehatScreenshot\Tests\Traits\ReflectionTrait; use DrevOps\BehatScreenshotExtension\Tokenizer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; @@ -15,18 +16,14 @@ #[CoversClass(Tokenizer::class)] class TokenizerTest extends TestCase { - /** - * Test scan tokens. - */ + use ReflectionTrait; + #[DataProvider('dataProviderScanTokens')] public function testScanTokens(string $textContainsTokens, array $expectedTokens): void { $tokens = Tokenizer::scanTokens($textContainsTokens); $this->assertEquals($expectedTokens, $tokens); } - /** - * Data for test scan tokens. - */ public static function dataProviderScanTokens(): array { return [ [ @@ -51,232 +48,157 @@ public static function dataProviderScanTokens(): array { ]; } - /** - * Test replace ext token. - * - * @param array $data - * Data to replace tokens. - * @param string $expected - * Expected string. - */ #[DataProvider('dataProviderReplaceExtToken')] - public function testReplaceExtToken(array $data, string $expected): void { - $replacement = Tokenizer::replaceExtToken('{ext}', 'ext', NULL, NULL, $data); + public function testReplaceExtToken(string $token, string $name, ?string $qualifier, ?string $format, array $data, string $expected): void { + $replacement = $this->callProtectedMethod(Tokenizer::class, 'replaceExtToken', [$token, $name, $qualifier, $format, $data]); $this->assertEquals($expected, $replacement); } - /** - * Provide data for test replace ext token. - */ public static function dataProviderReplaceExtToken(): array { return [ - [ - ['ext' => 'html'], - 'html', - ], - [ - ['ext' => 'png'], - 'png', - ], - [ - ['ext' => ''], - 'html', - ], - [ - [], - 'html', - ], + ['{ext}', 'ext', NULL, NULL, [], 'html'], + ['{ext}', 'ext', NULL, NULL, ['ext' => 'html'], 'html'], + ['{ext}', 'ext', NULL, NULL, ['ext' => 'png'], 'png'], + ['{ext}', 'ext', NULL, NULL, ['ext' => ''], 'html'], ]; } - /** - * Test replace step token. - */ #[DataProvider('dataProviderReplaceStepToken')] public function testReplaceStepToken(string $token, string $name, ?string $qualifier, ?string $format, array $data, string $expected): void { - $replacement = Tokenizer::replaceStepToken($token, $name, $qualifier, $format, $data); + $replacement = $this->callProtectedMethod(Tokenizer::class, 'replaceStepToken', [$token, $name, $qualifier, $format, $data]); $this->assertEquals($replacement, $expected); } - /** - * Data for test replace step token. - * - * @return array - * Data provider. - */ public static function dataProviderReplaceStepToken(): array { return [ + ['{step}', 'step', NULL, NULL, [], '{step}'], ['{step}', 'step', NULL, NULL, ['step_name' => 'Hello step'], 'Hello_step'], + + ['{step_name}', 'step', 'other', NULL, [], '{step_name}'], + ['{step_name}', 'step', 'other', NULL, ['step_name' => 'Hello step'], 'Hello_step'], ['{step_name}', 'step', 'name', NULL, ['step_name' => 'Hello step'], 'Hello_step'], + + ['{step_line}', 'step', 'other', NULL, [], '{step_line}'], + ['{step_line}', 'step', 'other', NULL, ['step_line' => 6], '{step_line}'], ['{step_line}', 'step', 'line', NULL, ['step_line' => 6], '6'], ['{step_line}', 'step', 'line', NULL, ['step_line' => '6'], '6'], ['{step_line}', 'step', 'line', '%03d', ['step_line' => '6'], '006'], ]; } - /** - * Test replace step token. - */ #[DataProvider('dataProviderReplaceDatetimeToken')] - public function testReplaceDatetimeToken(array $data, ?string $format, string $expected): void { - $replacement = Tokenizer::replaceDatetimeToken('{datetime}', 'datetime', NULL, $format, $data); - $this->assertEquals($replacement, $expected); + public function testReplaceDatetimeToken(string $token, string $name, ?string $qualifier, ?string $format, array $data, string $expected, ?string $exception = NULL): void { + if ($exception) { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($exception); + } - $data['time'] = 'foo'; - $this->expectExceptionMessage('Time must be an integer.'); - Tokenizer::replaceDatetimeToken('{datetime}', 'datetime', NULL, 'U', $data); + $replacement = $this->callProtectedMethod(Tokenizer::class, 'replaceDatetimeToken', [$token, $name, $qualifier, $format, $data]); + + if (!$exception) { + $this->assertEquals($replacement, $expected); + } } - /** - * Data provider for test replace datetime token. - */ public static function dataProviderReplaceDatetimeToken(): array { return [ - [['time' => strtotime('Tuesday, 12 March 2024 00:00:00')], 'U', '1710201600'], - [['time' => strtotime('Tuesday, 12 March 2024 00:00:00')], 'Y-m-d', '2024-03-12'], - [['time' => strtotime('Tuesday, 12 March 2024 00:00:00')], 'Y-m-d H:i:s', '2024-03-12 00:00:00'], - [['time' => strtotime('Tuesday, 12 March 2024 00:00:00')], NULL, '20240312_000000'], + ['{datetime}', 'datetime', NULL, NULL, [], '{datetime}'], + ['{datetime}', 'datetime', NULL, 'U', ['timestamp' => strtotime('Tuesday, 12 March 2024 00:00:00')], '1710201600'], + ['{datetime}', 'datetime', NULL, 'Y-m-d', ['timestamp' => strtotime('Tuesday, 12 March 2024 00:00:00')], '2024-03-12'], + ['{datetime}', 'datetime', NULL, 'Y-m-d H:i:s', ['timestamp' => strtotime('Tuesday, 12 March 2024 00:00:00')], '2024-03-12 00:00:00'], + ['{datetime}', 'datetime', NULL, NULL, ['timestamp' => strtotime('Tuesday, 12 March 2024 00:00:00')], '20240312_000000'], + ['{datetime}', 'datetime', NULL, NULL, ['timestamp' => '2'], '19700101_000002'], + ['{datetime}', 'datetime', NULL, NULL, ['timestamp' => 'foo'], '0', 'Timestamp must be greater than 0.'], + ['{datetime}', 'datetime', NULL, NULL, ['timestamp' => ['foo']], '', 'Timestamp must be numeric.'], ]; } - /** - * Test replace feature token. - */ #[DataProvider('dataProviderReplaceFeatureToken')] - public function testReplaceFeatureToken(array $data, string $expected): void { - $replacement = Tokenizer::replaceFeatureToken('{feature}', 'feature', 'file', NULL, $data); + public function testReplaceFeatureToken(string $token, string $name, ?string $qualifier, ?string $format, array $data, string $expected): void { + $replacement = $this->callProtectedMethod(Tokenizer::class, 'replaceFeatureToken', [$token, $name, $qualifier, $format, $data]); $this->assertEquals($expected, $replacement); } - /** - * Data provider for test replace feature. - */ public static function dataProviderReplaceFeatureToken(): array { return [ - [['feature_file' => 'stub-file.feature'], 'stub-file'], - [['feature_file' => 'path/example/stub-file.feature'], 'stub-file'], - [['feature_file' => NULL], '{feature}'], - [[], '{feature}'], + ['{feature}', 'feature', 'file', NULL, [], '{feature}'], + ['{feature}', 'feature', 'file', NULL, ['feature_file' => NULL], '{feature}'], + ['{feature}', 'feature', 'file', NULL, ['feature_file' => ''], '{feature}'], + ['{feature}', 'feature', 'file', NULL, ['feature_file' => 'stub-file.feature'], 'stub-file'], + ['{feature}', 'feature', 'file', NULL, ['feature_file' => 'path/example/stub-file.feature'], 'stub-file'], ]; } - /** - * Test replace fail token. - */ #[DataProvider('dataProviderReplaceFailToken')] - public function testReplaceFailToken(array $data, string $expected): void { - $replacement = Tokenizer::replaceFailToken('{fail}', 'fail', NULL, NULL, $data); + public function testReplaceFailToken(string $token, string $name, ?string $qualifier, ?string $format, array $data, string $expected): void { + $replacement = $this->callProtectedMethod(Tokenizer::class, 'replaceFailToken', [$token, $name, $qualifier, $format, $data]); $this->assertEquals($expected, $replacement); } - /** - * Data provider for test replace fail token. - */ public static function dataProviderReplaceFailToken(): array { return [ - [['fail_prefix' => 'HelloFail_'], 'HelloFail_'], - [[], '{fail}'], + ['{fail}', 'fail', NULL, NULL, [], '{fail}'], + ['{fail}', 'fail', NULL, NULL, ['fail_prefix' => ''], '{fail}'], + ['{fail}', 'fail', NULL, NULL, ['fail_prefix' => 'HelloFail_'], 'HelloFail_'], ]; } - /** - * Test replace url token. - */ #[DataProvider('dataProviderReplaceUrlToken')] - public function testReplaceUrlToken(string $token, ?string $qualifier, ?string $format, array $data, ?string $expected, bool $expectedException = FALSE): void { - if ($expectedException) { - $this->expectException(\Exception::class); - Tokenizer::replaceUrlToken($token, 'url', $qualifier, $format, $data); - } - else { - $replacement = Tokenizer::replaceUrlToken($token, 'url', $qualifier, $format, $data); - $this->assertEquals($expected, $replacement); + public function testReplaceUrlToken(string $token, string $name, ?string $qualifier, ?string $format, array $data, string $expected, ?string $exception = NULL): void { + if ($exception) { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage($exception); } + + $replacement = $this->callProtectedMethod(Tokenizer::class, 'replaceUrlToken', [$token, $name, $qualifier, $format, $data]); + $this->assertEquals($expected, $replacement); } - /** - * Data provider for test replace url token. - */ public static function dataProviderReplaceUrlToken(): array { $url = 'http://example.com/foo?foo=foo-value#hello-fragment'; return [ - [ - '{url}', - NULL, - NULL, - ['url' => $url], - urlencode($url), - ], - [ - '{url_relative}', - 'relative', - NULL, - ['url' => $url], - urlencode('foo?foo=foo-value#hello-fragment'), - ], - [ - '{url_origin}', - 'origin', - NULL, - ['url' => $url], - urlencode('http://example.com'), - ], - [ - '{url_domain}', - 'domain', - NULL, - ['url' => $url], - urlencode('example.com'), - ], - [ - '{url_path}', - 'path', - NULL, - ['url' => 'http://example.com/foo?foo=foo-value#hello-fragment'], - 'foo', - ], - [ - '{url_query}', - 'query', - NULL, - ['url' => $url], - urlencode('foo=foo-value'), - ], - [ - '{url_fragment}', - 'fragment', - NULL, - ['url' => $url], - 'hello-fragment', - ], - [ - '{url}', - NULL, - NULL, - ['url' => 'http:///example.com'], - NULL, - TRUE, - ], + ['{url}', 'url', NULL, NULL, [], '{url}'], + ['{url}', 'url', NULL, NULL, ['url' => $url], urlencode($url)], + ['{url}', 'url', NULL, 'relative', ['url' => $url], urlencode($url)], + ['{url}', 'url', NULL, NULL, ['url' => 'http:///example.com'], '', 'Could not parse URL "http:///example.com".'], + + ['{url_relative}', 'url', 'relative', NULL, ['url' => $url], urlencode('foo?foo=foo-value#hello-fragment')], + ['{url_relative}', 'url', NULL, NULL, ['url' => $url], urlencode($url)], + ['{url_relative}', 'url', NULL, NULL, [], '{url_relative}'], + + ['{url_origin}', 'url', 'origin', NULL, ['url' => $url], urlencode('http://example.com')], + ['{url_origin}', 'url', NULL, NULL, ['url' => $url], urlencode($url)], + ['{url_origin}', 'url', NULL, NULL, [], '{url_origin}'], + + ['{url_domain}', 'url', 'domain', NULL, ['url' => $url], urlencode('example.com')], + ['{url_domain}', 'url', NULL, NULL, ['url' => $url], urlencode($url)], + ['{url_domain}', 'url', NULL, NULL, [], '{url_domain}'], + + ['{url_path}', 'url', 'path', NULL, ['url' => 'http://example.com/foo?foo=foo-value#hello-fragment'], 'foo'], + ['{url_path}', 'url', NULL, NULL, ['url' => 'http://example.com/foo?foo=foo-value#hello-fragment'], urlencode($url)], + ['{url_path}', 'url', NULL, NULL, [], '{url_path}'], + + ['{url_query}', 'url', 'query', NULL, ['url' => $url], urlencode('foo=foo-value')], + ['{url_query}', 'url', NULL, NULL, ['url' => $url], urlencode($url)], + ['{url_query}', 'url', NULL, NULL, [], '{url_query}'], + + ['{url_fragment}', 'url', 'fragment', NULL, ['url' => $url], 'hello-fragment'], + ['{url_fragment}', 'url', NULL, NULL, ['url' => $url], urlencode($url)], + ['{url_fragment}', 'url', NULL, NULL, [], '{url_fragment}'], ]; } - /** - * Test replace tokens. - */ #[DataProvider('dataProviderReplaceTokens')] public function testReplaceTokens(string $stringContainsTokens, array $data, string $expected): void { $replacement = Tokenizer::replaceTokens($stringContainsTokens, $data); $this->assertEquals($expected, $replacement); } - /** - * Data provider for replace tokens. - */ public static function dataProviderReplaceTokens(): array { $data = [ 'fail_prefix' => 'foo-fail_', - 'time' => 1710219423, + 'timestamp' => 1710219423, 'ext' => 'png', 'url' => 'http://example.com/foo?foo=foo-value#hello-fragment', 'feature_file' => 'path/to/foo-file.feature', @@ -285,6 +207,11 @@ public static function dataProviderReplaceTokens(): array { ]; return [ + [ + 'somestring', + $data, + 'somestring', + ], [ '{datetime:U}.{fail_prefix}{feature_file}.feature_{step_line}.{ext}', $data,