From f844aa5a2ea2464b399327e8046491efe51c59c3 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:51:34 +0700 Subject: [PATCH 1/4] deps: bump allure-php-commons to ^2.4.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6cc477f..b6d4d63 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "php": "^8", "ext-json": "*", "codeception/codeception": "^5.0.3", - "allure-framework/allure-php-commons": "^2.3.1" + "allure-framework/allure-php-commons": "^2.4.0" }, "require-dev": { "psalm/plugin-phpunit": "^0.19.0 || ^0.20.1", From f2e79b36fe7135445dc0787f17e6c914688d3f05 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 1 May 2026 03:53:20 +0700 Subject: [PATCH 2/4] feat: implement titlePath --- src/AllureCodeception.php | 13 +-- src/Internal/CeptInfoBuilder.php | 12 +++ src/Internal/CestInfoBuilder.php | 6 +- src/Internal/GherkinInfoBuilder.php | 24 ++++- src/Internal/ModelFunctions.php | 82 ++++++++++++++++ src/Internal/TestInfo.php | 12 +++ src/Internal/TestLifecycle.php | 11 ++- src/Internal/UnitInfoBuilder.php | 1 + test/codeception/report-check/ReportTest.php | 24 +++++ .../unit/Internal/ModelFunctionsTest.php | 98 +++++++++++++++++++ 10 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 src/Internal/ModelFunctions.php create mode 100644 test/codeception/unit/Internal/ModelFunctionsTest.php diff --git a/src/AllureCodeception.php b/src/AllureCodeception.php index fc796af..fe7b6c6 100644 --- a/src/AllureCodeception.php +++ b/src/AllureCodeception.php @@ -311,12 +311,13 @@ public function stepAfter(StepEvent $stepEvent): void private function getTestLifecycle(): TestLifecycleInterface { return $this->testLifecycle ??= new TestLifecycle( - Allure::getLifecycle(), - Allure::getConfig()->getResultFactory(), - Allure::getConfig()->getStatusDetector(), - $this->getThreadDetector(), - Allure::getConfig()->getLinkTemplates(), - $_ENV, + rootDir: $this->getRootDir(), + lifecycle: Allure::getLifecycle(), + resultFactory: Allure::getConfig()->getResultFactory(), + statusDetector: Allure::getConfig()->getStatusDetector(), + threadDetector: $this->getThreadDetector(), + linkTemplates: Allure::getConfig()->getLinkTemplates(), + env: $_ENV, ); } } diff --git a/src/Internal/CeptInfoBuilder.php b/src/Internal/CeptInfoBuilder.php index caa4eb0..c5cecd7 100644 --- a/src/Internal/CeptInfoBuilder.php +++ b/src/Internal/CeptInfoBuilder.php @@ -6,9 +6,13 @@ use Codeception\Test\Cept; +use function array_pop; +use function realpath; + final class CeptInfoBuilder implements TestInfoBuilderInterface { public function __construct( + private string $rootDir, private Cept $test, ) { } @@ -16,11 +20,19 @@ public function __construct( #[\Override] public function build(?string $host, ?string $thread): TestInfo { + // May contain .. if the config is not in a parent directory. + $filePath = realpath($this->test->getFileName()); + $titlePath = ModelFunctions::getTitlePathByFile($this->rootDir, $filePath); + + // A cept file is a single test, so we're removing the file node from titlePath. + array_pop($titlePath); + return new TestInfo( originalTest: $this->test, signature: $this->test->getSignature(), class: $this->test->getName(), method: $this->test->getName(), + titlePath: $titlePath, host: $host, thread: $thread, ); diff --git a/src/Internal/CestInfoBuilder.php b/src/Internal/CestInfoBuilder.php index 634e427..1388f7e 100644 --- a/src/Internal/CestInfoBuilder.php +++ b/src/Internal/CestInfoBuilder.php @@ -19,11 +19,15 @@ public function __construct( #[\Override] public function build(?string $host, ?string $thread): TestInfo { + $class = $this->test->getTestInstance()::class; + $titlePath = ModelFunctions::getTitlePathByClass($class); + return new TestInfo( originalTest: $this->test, signature: $this->test->getSignature(), - class: $this->test->getTestInstance()::class, + class: $class, method: $this->test->getTestMethod(), + titlePath: $titlePath, dataLabel: $this->getDataLabel(), host: $host, thread: $thread, diff --git a/src/Internal/GherkinInfoBuilder.php b/src/Internal/GherkinInfoBuilder.php index 9d15b7d..3c41b18 100644 --- a/src/Internal/GherkinInfoBuilder.php +++ b/src/Internal/GherkinInfoBuilder.php @@ -6,11 +6,13 @@ use Codeception\Test\Gherkin; -use function is_string; +use function array_pop; +use function realpath; final class GherkinInfoBuilder implements TestInfoBuilderInterface { public function __construct( + private string $rootDir, private Gherkin $test, ) { } @@ -18,11 +20,31 @@ public function __construct( #[\Override] public function build(?string $host, ?string $thread): TestInfo { + // May contain .. if the config is not in a parent directory. + $filePath = realpath($this->test->getFileName()); + + /** + * @var list + */ + $titlePath = ModelFunctions::getTitlePathByFile($this->rootDir, $filePath); + + $featureName = $this->test->getFeature(); + if ($featureName) { + // Prefer a more human-readable feature name instead of the filename. + if ($titlePath) { + array_pop($titlePath); + $titlePath[] = $featureName; + } else { + $titlePath = [$featureName]; + } + } + return new TestInfo( originalTest: $this->test, signature: $this->test->getSignature(), class: $this->test->getFeature(), method: $this->test->getScenarioTitle(), + titlePath: $titlePath, host: $host, thread: $thread, ); diff --git a/src/Internal/ModelFunctions.php b/src/Internal/ModelFunctions.php new file mode 100644 index 0000000..26e83ab --- /dev/null +++ b/src/Internal/ModelFunctions.php @@ -0,0 +1,82 @@ + + */ + public static function getTitlePathByFile(string $base, string $path, bool $final = false): array + { + if (!$path) { + return []; + } + + $baseParts = explode(DIRECTORY_SEPARATOR, rtrim($base, DIRECTORY_SEPARATOR)); + $pathParts = explode(DIRECTORY_SEPARATOR, $path); + + if (!$base || $baseParts[0] !== $pathParts[0]) { + // The base is not provided or is on another disk (on Windows) + // or is not an absolute path. + // Fallback to CWD if not yet in fallback mode + if (!$final) { + return self::getTitlePathByFile(getcwd(), $path, true); + } + + // CWD didn't work too. Turn the absolute path into titlePath. + // Add leading '/' node on Linux/MAC to avoid confusion with + // well-formed titlePath values of other tests. + return $pathParts[0] + ? $pathParts + : [DIRECTORY_SEPARATOR, ...$pathParts]; + } + + do { + // Skipping identical parts of both paths. + array_shift($baseParts); + array_shift($pathParts); + } while ($baseParts && $pathParts && $baseParts[0] === $pathParts[0]); + + if (!$pathParts) { + // If the path contains less parts than the base (is a parent of the base) + // or is equal to the base, return empty titlePath. + return []; + } + + // At this point we have three cases: + // - $pathParts is empty: the path contains less parts than the base (is a + // parent of the base) or is equal to the base, return empty titlePath. + // - $baseParts is empty: the path is inside the base. + // The titlePath consists entirely of the remaining path parts. + // - $baseParts is not empty: the path is not in the base but they share a + // common ancestor. We consider this ancestor the proper root directory + // and return the remaining parts of the path as titlePath. + // Essentially, all three cases are treated identically. + return $pathParts; + } + + /** + * @return list + */ + public static function getTitlePathByClass(?string $class): array + { + return is_string($class) + ? [...array_filter(explode("\\", $class))] + : []; + } +} diff --git a/src/Internal/TestInfo.php b/src/Internal/TestInfo.php index 2c86a98..85586ec 100644 --- a/src/Internal/TestInfo.php +++ b/src/Internal/TestInfo.php @@ -6,11 +6,15 @@ final class TestInfo { + /** + * @param list $titlePath + */ public function __construct( private object $originalTest, private string $signature, private ?string $class = null, private ?string $method = null, + private array $titlePath = [], private ?string $dataLabel = null, private ?string $host = null, private ?string $thread = null, @@ -37,6 +41,14 @@ public function getMethod(): ?string return $this->method; } + /** + * @return list + */ + public function getTitlePath(): array + { + return $this->titlePath; + } + public function getDataLabel(): ?string { return $this->dataLabel; diff --git a/src/Internal/TestLifecycle.php b/src/Internal/TestLifecycle.php index 4c24bca..6cba707 100644 --- a/src/Internal/TestLifecycle.php +++ b/src/Internal/TestLifecycle.php @@ -48,6 +48,7 @@ final class TestLifecycle implements TestLifecycleInterface private WeakMap $stepStarts; public function __construct( + private string $rootDir, private AllureLifecycleInterface $lifecycle, private ResultFactoryInterface $resultFactory, private StatusDetectorInterface $statusDetector, @@ -115,8 +116,8 @@ private function getTestInfoBuilder(object $test): TestInfoBuilderInterface { return match (true) { $test instanceof Cest => new CestInfoBuilder($test), - $test instanceof Gherkin => new GherkinInfoBuilder($test), - $test instanceof Cept => new CeptInfoBuilder($test), + $test instanceof Gherkin => new GherkinInfoBuilder($this->rootDir, $test), + $test instanceof Cept => new CeptInfoBuilder($this->rootDir, $test), $test instanceof TestCaseWrapper => new UnitInfoBuilder($test), default => new UnknownInfoBuilder($test), }; @@ -142,16 +143,18 @@ public function create(): self #[\Override] public function updateTest(): self { + $test = $this->getCurrentTest(); $provider = new ModelProviderChain( new EnvProvider($this->env), ...SuiteProvider::createForChain($this->getCurrentSuite(), $this->linkTemplates), - ...TestInfoProvider::createForChain($this->getCurrentTest()), - ...$this->createModelProvidersForTest($this->getCurrentTest()->getOriginalTest()), + ...TestInfoProvider::createForChain($test), + ...$this->createModelProvidersForTest($test->getOriginalTest()), ); $this->lifecycle->updateTest( fn (TestResult $t) => $t ->setName($provider->getDisplayName()) ->setFullName($provider->getFullName()) + ->setTitlePath(...$test->getTitlePath()) ->setDescription($provider->getDescription()) ->setDescriptionHtml($provider->getDescriptionHtml()) ->addLinks(...$provider->getLinks()) diff --git a/src/Internal/UnitInfoBuilder.php b/src/Internal/UnitInfoBuilder.php index 8b2711d..a2c176c 100644 --- a/src/Internal/UnitInfoBuilder.php +++ b/src/Internal/UnitInfoBuilder.php @@ -33,6 +33,7 @@ public function build(?string $host, ?string $thread): TestInfo signature: $this->test->getSignature(), class: $class ?? null, method: $this->test->getMetadata()->getName(), + titlePath: ModelFunctions::getTitlePathByClass($class), dataLabel: $dataLabel, host: $host, thread: $thread, diff --git a/test/codeception/report-check/ReportTest.php b/test/codeception/report-check/ReportTest.php index 509d353..8bbe0bd 100644 --- a/test/codeception/report-check/ReportTest.php +++ b/test/codeception/report-check/ReportTest.php @@ -362,6 +362,30 @@ public static function providerExistingNodeValue(): iterable ), ['i expect condition 1.2'], ], + 'Title path of a unit test result' => [ + AnnotationTest::class, + 'testTitleAnnotation', + '$.titlePath', + [["Qameta", "Allure", "Codeception", "Test", "Report", "Unit", "AnnotationTest"]], + ], + 'Title path of a Cept result' => [ + "BasicScenario", + 'BasicScenario', + '$.titlePath', + [["test", "codeception-report", "functional"]], + ], + 'Title path of a Cest result' => [ + NestedStepsCest::class, + 'makeNestedSteps', + '$.titlePath', + [["Qameta", "Allure", "Codeception", "Test", "Report", "Functional", "NestedStepsCest"]], + ], + 'Title path of a Gherkin result' => [ + "Calculate absolute number", + 'negative number', + '$.titlePath', + [["test", "codeception-report", "acceptance", "Calculate absolute number"]], + ], ]; } diff --git a/test/codeception/unit/Internal/ModelFunctionsTest.php b/test/codeception/unit/Internal/ModelFunctionsTest.php new file mode 100644 index 0000000..2cd69f8 --- /dev/null +++ b/test/codeception/unit/Internal/ModelFunctionsTest.php @@ -0,0 +1,98 @@ +}> + */ + public static function providerGetTitlePathByFile(): iterable + { + return [ + "Direct child path" => ["/foo", "/foo/bar", ["bar"]], + "Nested child path" => ["/foo/bar", "/foo/bar/baz/qux", ["baz", "qux"]], + "Sibling path" => ["/foo/bar", "/foo/baz", ["baz"]], + "Path several levels up" => ["/foo/bar/baz", "/foo/qux", ["qux"]], + "Identical paths" => ["/foo/bar", "/foo/bar", []], + "Common root only" => ["/", "/foo/bar", ["foo", "bar"]], + ]; + } + + /** + * @dataProvider providerGetTitlePathByFileWithFallback + */ + public function testGetTitlePathByFile_FallbackToCwd( + string $base, + string $cwdRelPath, + array $expectedTitlePath + ): void { + // Quick Windows hack + $base = str_replace("/", DIRECTORY_SEPARATOR, $base); + $cwdRelPath = str_replace("/", DIRECTORY_SEPARATOR, $cwdRelPath); + + $path = getcwd() . DIRECTORY_SEPARATOR . $cwdRelPath; + + self::assertSame($expectedTitlePath, ModelFunctions::getTitlePathByFile($base, $path)); + } + + /** + * @return iterable}> + */ + public static function providerGetTitlePathByFileWithFallback(): iterable + { + return [ + "Different trees" => ["BAD:/foo", "foo", ["foo"]], + "Relative base" => ["foo", "foo/bar", ["foo", "bar"]], + "Empty base" => ["", "foo/bar", ["foo", "bar"]], + ]; + } + + /** + * @dataProvider providerGetTitlePathByClass + */ + public function testGetTitlePathByClass(?string $class, array $expectedTitlePath): void + { + self::assertSame($expectedTitlePath, ModelFunctions::getTitlePathByClass($class)); + } + + /** + * @return iterable}> + */ + public static function providerGetTitlePathByClass(): iterable + { + return [ + "No namespace" => ["foo", ["foo"]], + "Single-level namespace" => ["foo\\bar", ["foo", "bar"]], + "Nested namespaces" => ["foo\\bar\\baz", ["foo", "bar", "baz"]], + "Fully qualified name" => ["\\foo\\bar\\baz", ["foo", "bar", "baz"]], + "Empty string" => ["", []], + "Null" => [null, []], + ]; + } +} From 50232edf37b2bd5ec8f1c1887bfa911c6d02e6a0 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 1 May 2026 21:38:23 +0700 Subject: [PATCH 3/4] style: fix errors after rebase --- src/Internal/CeptInfoBuilder.php | 4 +- src/Internal/GherkinInfoBuilder.php | 4 +- src/Internal/ModelFunctions.php | 5 +- test/codeception/report-check/ReportTest.php | 76 +++++++++++++------ .../unit/Internal/ModelFunctionsTest.php | 7 +- 5 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/Internal/CeptInfoBuilder.php b/src/Internal/CeptInfoBuilder.php index c5cecd7..07af512 100644 --- a/src/Internal/CeptInfoBuilder.php +++ b/src/Internal/CeptInfoBuilder.php @@ -22,7 +22,9 @@ public function build(?string $host, ?string $thread): TestInfo { // May contain .. if the config is not in a parent directory. $filePath = realpath($this->test->getFileName()); - $titlePath = ModelFunctions::getTitlePathByFile($this->rootDir, $filePath); + $titlePath = $filePath !== false + ? ModelFunctions::getTitlePathByFile($this->rootDir, $filePath) + : []; // A cept file is a single test, so we're removing the file node from titlePath. array_pop($titlePath); diff --git a/src/Internal/GherkinInfoBuilder.php b/src/Internal/GherkinInfoBuilder.php index 3c41b18..eae4c76 100644 --- a/src/Internal/GherkinInfoBuilder.php +++ b/src/Internal/GherkinInfoBuilder.php @@ -26,7 +26,9 @@ public function build(?string $host, ?string $thread): TestInfo /** * @var list */ - $titlePath = ModelFunctions::getTitlePathByFile($this->rootDir, $filePath); + $titlePath = $filePath !== false + ? ModelFunctions::getTitlePathByFile($this->rootDir, $filePath) + : []; $featureName = $this->test->getFeature(); if ($featureName) { diff --git a/src/Internal/ModelFunctions.php b/src/Internal/ModelFunctions.php index 26e83ab..270f67f 100644 --- a/src/Internal/ModelFunctions.php +++ b/src/Internal/ModelFunctions.php @@ -35,7 +35,10 @@ public static function getTitlePathByFile(string $base, string $path, bool $fina // or is not an absolute path. // Fallback to CWD if not yet in fallback mode if (!$final) { - return self::getTitlePathByFile(getcwd(), $path, true); + $cwd = getcwd(); + if ($cwd !== false) { + return self::getTitlePathByFile($cwd, $path, true); + } } // CWD didn't work too. Turn the absolute path into titlePath. diff --git a/test/codeception/report-check/ReportTest.php b/test/codeception/report-check/ReportTest.php index 8bbe0bd..b04201c 100644 --- a/test/codeception/report-check/ReportTest.php +++ b/test/codeception/report-check/ReportTest.php @@ -98,7 +98,7 @@ public static function providerSingleNodeValueStartsFromString(): iterable 'Error message in test case without steps' => [ StepsTest::class, 'testNoStepsError', - fn (object $tr): mixed => self::property(self::objectProperty($tr, 'statusDetails'), 'message'), + fn (object $tr): mixed => self::statusMessage($tr), "Error\nException(0)", ], ]; @@ -168,7 +168,7 @@ public static function providerExistingNodeValue(): iterable 'Successful test case without steps: no steps' => [ StepsTest::class, 'testNoStepsSuccess', - fn (object $tr): mixed => self::objectListProperty($tr, 'steps'), + fn (object $tr): mixed => self::steps($tr), [], ], 'Error in test case without steps' => [ @@ -180,7 +180,7 @@ public static function providerExistingNodeValue(): iterable 'Failure message in test case without steps' => [ StepsTest::class, 'testNoStepsFailure', - fn (object $tr): mixed => self::property(self::objectProperty($tr, 'statusDetails'), 'message'), + fn (object $tr): mixed => self::statusMessage($tr), 'Failure', ], 'Test case without steps skipped' => [ @@ -192,7 +192,7 @@ public static function providerExistingNodeValue(): iterable 'Skipped message in test case without steps' => [ StepsTest::class, 'testNoStepsSkipped', - fn (object $tr): mixed => self::property(self::objectProperty($tr, 'statusDetails'), 'message'), + fn (object $tr): mixed => self::statusMessage($tr), 'Skipped', ], 'Successful test case with single step: status' => [ @@ -248,7 +248,7 @@ public static function providerExistingNodeValue(): iterable 'testTwoSuccessfulSteps', fn (object $tr): mixed => array_map( fn (object $s): mixed => self::property($s, 'status'), - self::objectListProperty($tr, 'steps'), + self::steps($tr), ), ['passed', 'passed'], ], @@ -257,7 +257,7 @@ public static function providerExistingNodeValue(): iterable 'testTwoSuccessfulSteps', fn (object $tr): mixed => array_map( fn (object $s): mixed => self::property($s, 'name'), - self::objectListProperty($tr, 'steps'), + self::steps($tr), ), ['step 1 name', 'step 2 name'], ], @@ -270,7 +270,7 @@ public static function providerExistingNodeValue(): iterable 'First step in test case with two steps fails: message' => [ StepsTest::class, 'testTwoStepsFirstFails', - fn (object $tr): mixed => self::property(self::objectProperty($tr, 'statusDetails'), 'message'), + fn (object $tr): mixed => self::statusMessage($tr), 'Failure', ], 'First step in test case with two steps fails: step status' => [ @@ -294,7 +294,7 @@ public static function providerExistingNodeValue(): iterable 'Second step in test case with two steps fails: message' => [ StepsTest::class, 'testTwoStepsSecondFails', - fn (object $tr): mixed => self::property(self::objectProperty($tr, 'statusDetails'), 'message'), + fn (object $tr): mixed => self::statusMessage($tr), 'Failure', ], 'Second step in test case with two steps fails: step status' => [ @@ -302,7 +302,7 @@ public static function providerExistingNodeValue(): iterable 'testTwoStepsSecondFails', fn (object $tr): mixed => array_map( fn (object $s): mixed => self::property($s, 'status'), - self::objectListProperty($tr, 'steps'), + self::steps($tr), ), ['passed', 'failed'], ], @@ -310,8 +310,8 @@ public static function providerExistingNodeValue(): iterable StepsTest::class, 'testTwoStepsSecondFails', fn (object $tr): mixed => array_map( - fn (object $s): mixed => self::property($s, 'name'), - self::objectListProperty($tr, 'steps'), + fn (object $s): mixed => self::getName($s), + self::steps($tr), ), ['step 1 name', 'step 2 name'], ], @@ -326,7 +326,7 @@ public static function providerExistingNodeValue(): iterable 'makeNestedSteps', fn (object $tr): mixed => array_map( fn (object $s): mixed => self::property($s, 'name'), - self::objectListProperty(self::singleStep($tr), 'steps'), + self::steps(self::singleStep($tr)), ), ['i expect condition 1', 'Step 1.1', 'Step 1.2'], ], @@ -335,7 +335,7 @@ public static function providerExistingNodeValue(): iterable 'makeNestedSteps', fn (object $tr): mixed => array_map( fn (object $s): mixed => self::property($s, 'name'), - self::objectListProperty(self::findStep(self::singleStep($tr), "Step 1.1"), 'steps'), + self::steps(self::findStep(self::singleStep($tr), "Step 1.1")), ), ['i expect condition 1.1', 'Step 1.1.1'], ], @@ -357,34 +357,34 @@ public static function providerExistingNodeValue(): iterable NestedStepsCest::class, 'makeNestedSteps', fn (object $tr): mixed => array_map( - fn (object $s): mixed => self::property($s, 'name'), - self::objectListProperty(self::findStep(self::singleStep($tr), "Step 1.2"), 'steps'), + fn (object $s): mixed => self::getName($s), + self::steps(self::findStep(self::singleStep($tr), "Step 1.2")), ), ['i expect condition 1.2'], ], 'Title path of a unit test result' => [ AnnotationTest::class, 'testTitleAnnotation', - '$.titlePath', - [["Qameta", "Allure", "Codeception", "Test", "Report", "Unit", "AnnotationTest"]], + fn (object $tr): mixed => $tr->titlePath, + ["Qameta", "Allure", "Codeception", "Test", "Report", "Unit", "AnnotationTest"], ], 'Title path of a Cept result' => [ "BasicScenario", 'BasicScenario', - '$.titlePath', - [["test", "codeception-report", "functional"]], + fn (object $tr): mixed => $tr->titlePath, + ["test", "codeception-report", "functional"], ], 'Title path of a Cest result' => [ NestedStepsCest::class, 'makeNestedSteps', - '$.titlePath', - [["Qameta", "Allure", "Codeception", "Test", "Report", "Functional", "NestedStepsCest"]], + fn (object $tr): mixed => $tr->titlePath, + ["Qameta", "Allure", "Codeception", "Test", "Report", "Functional", "NestedStepsCest"], ], 'Title path of a Gherkin result' => [ "Calculate absolute number", 'negative number', - '$.titlePath', - [["test", "codeception-report", "acceptance", "Calculate absolute number"]], + fn (object $tr): mixed => $tr->titlePath, + ["test", "codeception-report", "acceptance", "Calculate absolute number"], ], ]; } @@ -468,6 +468,15 @@ private static function property(object $value, string $property): mixed return $value->{$property}; } + private static function stringProperty(object $value, string $property): string + { + $result = self::property($value, $property); + + self::assertIsString($result); + + return $result; + } + private static function objectProperty(object $value, string $property): object { $result = self::property($value, $property); @@ -487,4 +496,25 @@ private static function objectListProperty(object $value, string $property): arr /** @var list */ return array_values($result); } + + private static function getName(object $value): string + { + return self::stringProperty($value, "name"); + } + + private static function statusMessage(object $value): string + { + return self::stringProperty( + self::objectProperty($value, "statusDetails"), + "message", + ); + } + + /** + * @return list + */ + private static function steps(object $parent): array + { + return self::objectListProperty($parent, "steps"); + } } diff --git a/test/codeception/unit/Internal/ModelFunctionsTest.php b/test/codeception/unit/Internal/ModelFunctionsTest.php index 2cd69f8..266dd66 100644 --- a/test/codeception/unit/Internal/ModelFunctionsTest.php +++ b/test/codeception/unit/Internal/ModelFunctionsTest.php @@ -12,7 +12,7 @@ use const DIRECTORY_SEPARATOR; -class ModelFunctionsTest extends Unit +final class ModelFunctionsTest extends Unit { /** * @dataProvider providerGetTitlePathByFile @@ -55,8 +55,11 @@ public function testGetTitlePathByFile_FallbackToCwd( // Quick Windows hack $base = str_replace("/", DIRECTORY_SEPARATOR, $base); $cwdRelPath = str_replace("/", DIRECTORY_SEPARATOR, $cwdRelPath); + $cwd = getcwd(); - $path = getcwd() . DIRECTORY_SEPARATOR . $cwdRelPath; + self::assertIsString($cwd); + + $path = $cwd . DIRECTORY_SEPARATOR . $cwdRelPath; self::assertSame($expectedTitlePath, ModelFunctions::getTitlePathByFile($base, $path)); } From a6e94740120ce04aed86c3c9d9df434bbd94e5c9 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 1 May 2026 21:42:26 +0700 Subject: [PATCH 4/4] test: fix conflicting name in ReportTest.php --- test/codeception/report-check/ReportTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/codeception/report-check/ReportTest.php b/test/codeception/report-check/ReportTest.php index b04201c..0bfa9e1 100644 --- a/test/codeception/report-check/ReportTest.php +++ b/test/codeception/report-check/ReportTest.php @@ -310,7 +310,7 @@ public static function providerExistingNodeValue(): iterable StepsTest::class, 'testTwoStepsSecondFails', fn (object $tr): mixed => array_map( - fn (object $s): mixed => self::getName($s), + fn (object $s): mixed => self::allureName($s), self::steps($tr), ), ['step 1 name', 'step 2 name'], @@ -357,7 +357,7 @@ public static function providerExistingNodeValue(): iterable NestedStepsCest::class, 'makeNestedSteps', fn (object $tr): mixed => array_map( - fn (object $s): mixed => self::getName($s), + fn (object $s): mixed => self::allureName($s), self::steps(self::findStep(self::singleStep($tr), "Step 1.2")), ), ['i expect condition 1.2'], @@ -497,7 +497,7 @@ private static function objectListProperty(object $value, string $property): arr return array_values($result); } - private static function getName(object $value): string + private static function allureName(object $value): string { return self::stringProperty($value, "name"); }