diff --git a/README.md b/README.md index 6347947..58b8c59 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ default: fileNamePrefix: report resultFilePerSuite: true outputDir: %paths.base%/build/tests + screenshotExtension: Bex\Behat\ScreenshotExtension ``` Then you can run: @@ -49,6 +50,7 @@ bin/behat -f cucumber_json Only applicable when `resultFilePerSuite` is not enabled. - `resultFilePerSuite` _(optional)_: The default behaviour is to generate a single report named `all.json`. If this option is set to `true`, a report will be created per behat suite. +- `screenshotExtension` _(optional)_: The name of the extension to be used to take screenshots. ## Licence diff --git a/features/calculator.feature b/features/calculator.feature index 52203f9..f5e5935 100644 --- a/features/calculator.feature +++ b/features/calculator.feature @@ -54,7 +54,8 @@ Feature: Calculator example "result": { "duration": 12345, "status": "passed" - } + }, + "embeddings": [] }, { "keyword": "When", @@ -66,7 +67,8 @@ Feature: Calculator example "result": { "duration": 12345, "status": "passed" - } + }, + "embeddings": [] }, { "keyword": "Then", @@ -83,7 +85,8 @@ Feature: Calculator example "result": { "duration": 12345, "status": "passed" - } + }, + "embeddings": [] } ], "type": "scenario" diff --git a/features/eat-cukes.feature b/features/eat-cukes.feature index 93285a8..d2d8168 100644 --- a/features/eat-cukes.feature +++ b/features/eat-cukes.feature @@ -51,7 +51,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::passing1()" - } + }, + "embeddings": [] }, { "result": { @@ -68,7 +69,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::eat()" - } + }, + "embeddings": [] }, { "result": { @@ -85,7 +87,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::hungry()" - } + }, + "embeddings": [] } ], "type": "scenario_outline" @@ -112,7 +115,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::passing1()" - } + }, + "embeddings": [] }, { "result": { @@ -129,7 +133,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::eat()" - } + }, + "embeddings": [] }, { "result": { @@ -147,7 +152,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::hungry()" - } + }, + "embeddings": [] } ], "type": "scenario_outline" @@ -174,7 +180,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::passing1()" - } + }, + "embeddings": [] }, { "result": { @@ -191,7 +198,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::eat()" - } + }, + "embeddings": [] }, { "result": { @@ -209,7 +217,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::hungry()" - } + }, + "embeddings": [] } ], "type": "scenario_outline" @@ -236,7 +245,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::passing1()" - } + }, + "embeddings": [] }, { "result": { @@ -253,7 +263,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::eat()" - } + }, + "embeddings": [] }, { "result": { @@ -270,7 +281,8 @@ Feature: Eat cukes Example } ], "location": "ExampleFeatureContext::hungry()" - } + }, + "embeddings": [] } ], "type": "scenario_outline" diff --git a/src/Extension.php b/src/Extension.php index 04251b3..16b6257 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -31,6 +31,7 @@ public function configure(ArrayNodeDefinition $builder): void $builder->children()->scalarNode('fileNamePrefix')->defaultValue(''); $builder->children()->scalarNode('outputDir')->defaultValue('build/tests'); $builder->children()->scalarNode('fileName'); + $builder->children()->scalarNode('screenshotExtension'); $builder->children()->booleanNode('resultFilePerSuite')->defaultFalse(); } @@ -41,6 +42,24 @@ public function load(ContainerBuilder $container, array $config): void $definition->addArgument($config['fileNamePrefix']); $definition->addArgument($config['outputDir']); + // Integration with other Behat extensions that provide screenshot services. + $imageUploaderService = null; + if (!empty($config['screenshotExtension'])) { + $tags = $container->findTaggedServiceIds('screenshot.service'); + foreach (array_keys($tags) as $id) { + // Check if the container has the definitions for the service. + if ($container->hasDefinition($id)) { + $service = $container->get($id); + // Check if the configuration for the screenshot extension matches the namespace of the service. + if (strpos(get_class($service), $config['screenshotExtension']) !== false) { + $imageUploaderService = $service; + break; + } + } + } + } + $definition->addArgument($imageUploaderService); + if (!empty($config['fileName'])) { $definition->addMethodCall('setFileName', [$config['fileName']]); } diff --git a/src/Formatter/Formatter.php b/src/Formatter/Formatter.php index b58c671..4161a74 100644 --- a/src/Formatter/Formatter.php +++ b/src/Formatter/Formatter.php @@ -48,11 +48,17 @@ class Formatter implements FormatterInterface /** @var bool */ private $resultFilePerSuite = false; - public function __construct(string $fileNamePrefix, string $outputDir) + /** + * The screenshot service, if available. + */ + private $screenshotService; + + public function __construct(string $fileNamePrefix, string $outputDir, $screenshotService = null) { $this->renderer = new JsonRenderer($this); $this->printer = new FileOutputPrinter($fileNamePrefix, $outputDir); $this->timer = new Timer(); + $this->screenshotService = $screenshotService; } /** @inheritdoc */ @@ -74,6 +80,35 @@ public static function getSubscribedEvents(): array ]; } + /** + * Attaches screenshots to steps. + */ + private function attachScreenshots() { + if (!$this->screenshotService) { + return; + } + + $steps = $this->currentScenario->getSteps(); + if (empty($steps)) { + return; + } + + $files = $this->screenshotService->getImages(); + if (empty($files)) { + return; + } + array_reverse($files); + + foreach ($steps as &$step) { + if ($step->getResultCode() !== 0) { + $file = array_pop($files); + if (!empty($file)) { + $step->addEmbedding($file); + } + } + } + } + /** @inheritdoc */ public function setFileName($fileName): void { @@ -225,6 +260,7 @@ public function onAfterScenarioTested(BehatEvent\AfterScenarioTested $event): vo $this->currentFeature->addPassedScenario(); } else { $this->currentFeature->addFailedScenario(); + $this->attachScreenshots(); } $this->currentScenario->setPassed($event->getTestResult()->isPassed()); @@ -271,6 +307,7 @@ public function onAfterOutlineTested(BehatEvent\AfterOutlineTested $event): void $this->currentFeature->addPassedScenario(); } else { $this->currentFeature->addFailedScenario(); + $this->attachScreenshots(); } $example->setPassed($scenarioPassed); diff --git a/src/Node/Step.php b/src/Node/Step.php index 60ea84f..2047911 100644 --- a/src/Node/Step.php +++ b/src/Node/Step.php @@ -75,6 +75,11 @@ class Step */ private $output; + /** + * @var array + */ + private $embeddings = []; + /** * @var ?Definition */ @@ -165,6 +170,26 @@ public function setLine(int $line): void $this->line = $line; } + /** + * @return array + */ + public function getEmbeddings() + { + return $this->embeddings; + } + + /** + * @param string $url + */ + public function addEmbedding($url) + { + $this->embeddings[] = [ + 'mime_type' => 'image/url', + 'name' => 'Screenshot', + 'data' => base64_encode($url) + ]; + } + /** * @return mixed */ diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index 1319365..6445c3a 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -150,6 +150,7 @@ protected function processStep(Node\Step $step): array 'line' => $step->getLine(), 'match' => $step->getMatch(), 'result' => $step->getProcessedResult(), + 'embeddings' => $step->getEmbeddings(), ]; }