diff --git a/src/Codeception/Reporter/HtmlReporter.php b/src/Codeception/Reporter/HtmlReporter.php index 131a369eba..3b7da22a29 100644 --- a/src/Codeception/Reporter/HtmlReporter.php +++ b/src/Codeception/Reporter/HtmlReporter.php @@ -19,6 +19,7 @@ use SebastianBergmann\Template\Template; use SebastianBergmann\Timer\Timer; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use function trigger_error; class HtmlReporter implements EventSubscriberInterface { @@ -45,8 +46,6 @@ class HtmlReporter implements EventSubscriberInterface protected string $templatePath; - protected array $failures = []; - private string $reportFile; private Timer $timer; @@ -88,41 +87,52 @@ public function beforeSuite(SuiteEvent $event): void public function testSuccess(TestEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioSuccess'); + $this->printTestEvent($event, 'scenarioSuccess'); } public function testError(FailEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioFailed'); + $this->printTestEvent($event, 'scenarioFailed'); } public function testFailure(FailEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioFailed'); + $this->printTestEvent($event, 'scenarioFailed'); } public function testWarning(FailEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioSuccess'); + $this->printTestEvent($event, 'scenarioSuccess'); } public function testSkipped(FailEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioSkipped'); + $this->printTestEvent($event, 'scenarioSkipped'); } public function testIncomplete(FailEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioIncomplete'); + $this->printTestEvent($event, 'scenarioIncomplete'); } public function testUseless(FailEvent $event): void { - $this->printTestResult($event->getTest(), $event->getTime(), 'scenarioUseless'); + $this->printTestEvent($event, 'scenarioUseless'); } - public function printTestResult(Test $test, float $time, string $scenarioStatus): void + public function printTestEvent(TestEvent $event, string $scenarioStatus): void { + $failure = ''; + + $test = $event->getTest(); + if ($event instanceof FailEvent) { + $failTemplate = new Template( + $this->templatePath . 'fail.html' + ); + $failTemplate->setVar(['fail' => nl2br($event->getFail()->getMessage())]); + $failure = $failTemplate->render() . PHP_EOL; + } + $steps = []; if ($test instanceof ScenarioDriven) { @@ -154,23 +164,88 @@ public function printTestResult(Test $test, float $time, string $scenarioStatus) } } + $png = ''; + $html = ''; + if ($test instanceof TestInterface) { + $reports = $test->getMetadata()->getReports(); + if (isset($reports['png'])) { + $localPath = PathResolver::getRelativeDir($reports['png'], codecept_output_dir()); + $png = "
failure screenshot
"; + } + if (isset($reports['html'])) { + $localPath = PathResolver::getRelativeDir($reports['html'], codecept_output_dir()); + $html = "See HTML snapshot of a failed page"; + } + } + + $toggle = $stepsBuffer ? '+' : ''; + + $testString = htmlspecialchars(ucfirst(Descriptor::getTestAsString($test)), ENT_QUOTES | ENT_SUBSTITUTE); + $testString = preg_replace('~^([\s\w\\\]+):\s~', '$1 » ', $testString); + $scenarioTemplate = new Template( $this->templatePath . 'scenario.html' ); + $scenarioTemplate->setVar( + [ + 'id' => ++$this->id, + 'name' => $testString, + 'scenarioStatus' => $scenarioStatus, + 'steps' => $stepsBuffer, + 'toggle' => $toggle, + 'failure' => $failure, + 'png' => $png, + 'html' => $html, + 'time' => round($event->getTime(), 2), + ] + ); - $failures = ''; - $name = Descriptor::getTestSignatureUnique($test); - if (isset($this->failures[$name])) { - $failTemplate = new Template( - $this->templatePath . 'fail.html' - ); - foreach ($this->failures[$name] as $failure) { - $failTemplate->setVar(['fail' => nl2br($failure)]); - $failures .= $failTemplate->render() . PHP_EOL; + $this->scenarios .= $scenarioTemplate->render(); + } + + public function printTestResult(Test $test, float $time, string $scenarioStatus): void + { + //keep this method for backwards compatibility, remove in Codeception 6.0 + trigger_error( + __METHOD__ . ' is deprecated, please use printTestEvent instead', + E_USER_DEPRECATED, + ); + + $steps = []; + + if ($test instanceof ScenarioDriven) { + $steps = $test->getScenario()->getSteps(); + } + + $stepsBuffer = ''; + $subStepsRendered = []; + + foreach ($steps as $step) { + $metaStep = $step->getMetaStep(); + if ($metaStep) { + $key = $this->getMetaStepKey($metaStep); + $subStepsRendered[$key][] = $this->renderStep($step); + } + } + + foreach ($steps as $step) { + $metaStep = $step->getMetaStep(); + if ($metaStep) { + $key = $this->getMetaStepKey($metaStep); + if (! empty($subStepsRendered[$key])) { + $subStepsBuffer = implode('', $subStepsRendered[$key]); + unset($subStepsRendered[$key]); + $stepsBuffer .= $this->renderSubsteps($step->getMetaStep(), $subStepsBuffer); + } + } else { + $stepsBuffer .= $this->renderStep($step); } - $this->failures[$name] = []; } + $scenarioTemplate = new Template( + $this->templatePath . 'scenario.html' + ); + $png = ''; $html = ''; if ($test instanceof TestInterface) { @@ -197,7 +272,7 @@ public function printTestResult(Test $test, float $time, string $scenarioStatus) 'scenarioStatus' => $scenarioStatus, 'steps' => $stepsBuffer, 'toggle' => $toggle, - 'failure' => $failures, + 'failure' => '', 'png' => $png, 'html' => $html, 'time' => round($time, 2) diff --git a/tests/cli/RunCest.php b/tests/cli/RunCest.php index 31e40248b1..0466efc4bd 100755 --- a/tests/cli/RunCest.php +++ b/tests/cli/RunCest.php @@ -779,6 +779,7 @@ public function runHtmlWithPhpBrowserCheckReport(CliGuy $I) $I->seeInShellOutput('Response: ' . $expectedReportAbsFilename); $I->seeFileFound('report.html', $expectedRelReportPath); $I->seeInThisFile("See HTML snapshot of a failed page"); + $I->seeInThisFile('Failed asserting that on page /'); } private function htmlReportRegexCheckProvider(): array