diff --git a/allure-java-commons/README.md b/allure-java-commons/README.md index 5d1786906..7e1dc9e29 100644 --- a/allure-java-commons/README.md +++ b/allure-java-commons/README.md @@ -45,6 +45,27 @@ Allure.step("Create order", () -> { High-level attachment APIs create attachment steps so attachments stay ordered with surrounding steps in Allure Report. +### Preconditions and Expected Results + +Report preconditions and expected results separately from the test description: + +```java +Allure.precondition("User is **logged in**"); +Allure.expectedResult("**Dashboard** is displayed"); + +// Use explicit HTML for rich formatting. +Allure.preconditionHtml("

User is logged in

"); +Allure.expectedResultHtml("

Dashboard is displayed

"); +``` + +These helpers follow description metadata targeting: calls in a test update that test; calls in a before fixture +provide scope defaults for its linked tests. Each field inherits independently, and explicit test values, +including empty strings, take precedence over scope defaults. Calls outside a running executable or in an +after fixture have no effect. + +Markdown and HTML values are stored independently. When both are provided, HTML takes precedence in reports +that support these fields, including Allure Report 3. + ## Annotation API The module provides annotations such as `@Step`, `@Attachment`, `@Owner`, `@Epic`, `@Feature`, and `@Story`. diff --git a/allure-java-commons/src/main/java/io/qameta/allure/Allure.java b/allure-java-commons/src/main/java/io/qameta/allure/Allure.java index eec88334f..f2cf84582 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/Allure.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/Allure.java @@ -413,6 +413,54 @@ public static void descriptionHtml(final String descriptionHtml) { getLifecycle().updateTestMetadata(metadata -> metadata.setDescriptionHtml(descriptionHtml)); } + /** + * Adds precondition to the current test, or to the scope of a running before fixture. + * Takes no effect if no executable is running or an after fixture is running. + * Expects Markdown content. + * + * @param precondition the precondition in Markdown format + * @see #preconditionHtml(String) + */ + public static void precondition(final String precondition) { + getLifecycle().updateTestMetadata(metadata -> metadata.setPrecondition(precondition)); + } + + /** + * Adds HTML precondition to the current test, or to the scope of a running before fixture. + * Takes no effect if no executable is running or an after fixture is running. + * The HTML value takes precedence over Markdown when rendered in the report. + * + * @param preconditionHtml the HTML precondition in HTML format + * @see #precondition(String) + */ + public static void preconditionHtml(final String preconditionHtml) { + getLifecycle().updateTestMetadata(metadata -> metadata.setPreconditionHtml(preconditionHtml)); + } + + /** + * Adds expected result to the current test, or to the scope of a running before fixture. + * Takes no effect if no executable is running or an after fixture is running. + * Expects Markdown content. + * + * @param expectedResult the expected result in Markdown format + * @see #expectedResultHtml(String) + */ + public static void expectedResult(final String expectedResult) { + getLifecycle().updateTestMetadata(metadata -> metadata.setExpectedResult(expectedResult)); + } + + /** + * Adds HTML expected result to the current test, or to the scope of a running before fixture. + * Takes no effect if no executable is running or an after fixture is running. + * The HTML value takes precedence over Markdown when rendered in the report. + * + * @param expectedResultHtml the HTML expected result in HTML format + * @see #expectedResult(String) + */ + public static void expectedResultHtml(final String expectedResultHtml) { + getLifecycle().updateTestMetadata(metadata -> metadata.setExpectedResultHtml(expectedResultHtml)); + } + /** * Adds an error from the given throwable that belongs to the test run rather than to a test or fixture. * The error is passed synchronously to the configured results writer before this method returns. diff --git a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java index 81180aa30..c378cd712 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java @@ -1438,6 +1438,18 @@ private static void mergeScopeMetadata(final ScopeResult scope, final TestResult if (Objects.isNull(testResult.getDescriptionHtml())) { testResult.setDescriptionHtml(scope.getDescriptionHtml()); } + if (Objects.isNull(testResult.getPrecondition())) { + testResult.setPrecondition(scope.getPrecondition()); + } + if (Objects.isNull(testResult.getPreconditionHtml())) { + testResult.setPreconditionHtml(scope.getPreconditionHtml()); + } + if (Objects.isNull(testResult.getExpectedResult())) { + testResult.setExpectedResult(scope.getExpectedResult()); + } + if (Objects.isNull(testResult.getExpectedResultHtml())) { + testResult.setExpectedResultHtml(scope.getExpectedResultHtml()); + } } private static TestResultContainer toScopeContainer(final ScopeResult scope) { diff --git a/allure-java-commons/src/test/java/io/qameta/allure/AllureTest.java b/allure-java-commons/src/test/java/io/qameta/allure/AllureTest.java index 6401f50b5..16d8e5028 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/AllureTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/AllureTest.java @@ -398,6 +398,150 @@ void shouldAddDescriptionHtml() { .containsExactly(descriptionHtml); } + /** + * Tests can report precondition independently from their description. + */ + @Description + @Test + void shouldAddPrecondition() { + final String value = "User is **logged in**"; + + final AllureResults results = runWithinTestContext(() -> Allure.precondition(value)); + + assertThat(results.getTestResults()) + .extracting(TestResult::getPrecondition) + .containsExactly(value); + } + + /** + * Tests can report precondition html independently from their description. + */ + @Description + @Test + void shouldAddPreconditionHtml() { + final String value = "

User is logged in

"; + + final AllureResults results = runWithinTestContext(() -> Allure.preconditionHtml(value)); + + assertThat(results.getTestResults()) + .extracting(TestResult::getPreconditionHtml) + .containsExactly(value); + } + + /** + * Tests can report expected result independently from their description. + */ + @Description + @Test + void shouldAddExpectedResult() { + final String value = "**Dashboard** is displayed"; + + final AllureResults results = runWithinTestContext(() -> Allure.expectedResult(value)); + + assertThat(results.getTestResults()) + .extracting(TestResult::getExpectedResult) + .containsExactly(value); + } + + /** + * Tests can report expected result html independently from their description. + */ + @Description + @Test + void shouldAddExpectedResultHtml() { + final String value = "

Dashboard is displayed

"; + + final AllureResults results = runWithinTestContext(() -> Allure.expectedResultHtml(value)); + + assertThat(results.getTestResults()) + .extracting(TestResult::getExpectedResultHtml) + .containsExactly(value); + } + + /** + * Preconditions and expected results set in shared setup become defaults for linked tests. + */ + @Description + @Test + void shouldInheritPreconditionAndExpectedResultFromBeforeFixture() { + final AllureResults results = runWithPreconditionAndExpectedResult(new TestResult()); + + final TestResult result = results.getTestResults().get(0); + assertThat(result) + .hasFieldOrPropertyWithValue("precondition", "Scope precondition") + .hasFieldOrPropertyWithValue("preconditionHtml", "

Scope precondition

") + .hasFieldOrPropertyWithValue("expectedResult", "Scope expected result") + .hasFieldOrPropertyWithValue("expectedResultHtml", "

Scope expected result

"); + } + + /** + * Explicit test metadata, including empty strings, takes precedence over shared setup defaults. + */ + @Description + @Test + void shouldPreserveExplicitPreconditionAndExpectedResult() { + final TestResult test = new TestResult() + .setPrecondition("") + .setPreconditionHtml("") + .setExpectedResult("") + .setExpectedResultHtml(""); + + final AllureResults results = runWithPreconditionAndExpectedResult(test); + + final TestResult result = results.getTestResults().get(0); + assertThat(result) + .hasFieldOrPropertyWithValue("precondition", "") + .hasFieldOrPropertyWithValue("preconditionHtml", "") + .hasFieldOrPropertyWithValue("expectedResult", "") + .hasFieldOrPropertyWithValue("expectedResultHtml", ""); + } + + /** + * Each Markdown or HTML field inherits independently when a test overrides only part of its metadata. + */ + @Description + @Test + void shouldInheritOnlyMissingPreconditionAndExpectedResultFields() { + final TestResult test = new TestResult() + .setPrecondition("Test precondition") + .setExpectedResultHtml("

Test expected result

"); + + final AllureResults results = runWithPreconditionAndExpectedResult(test); + + final TestResult result = results.getTestResults().get(0); + assertThat(result) + .hasFieldOrPropertyWithValue("precondition", "Test precondition") + .hasFieldOrPropertyWithValue("preconditionHtml", "

Scope precondition

") + .hasFieldOrPropertyWithValue("expectedResult", "Scope expected result") + .hasFieldOrPropertyWithValue("expectedResultHtml", "

Test expected result

"); + } + + private AllureResults runWithPreconditionAndExpectedResult(final TestResult test) { + return runTests(lifecycle -> { + final AllureExternalKey scopeKey = AllureExternalKey.of( + AllureTest.class, SCOPE_KEY_NAMESPACE, UUID.randomUUID().toString() + ); + final AllureExternalKey fixtureKey = AllureExternalKey.of( + AllureTest.class, FIXTURE_KEY_NAMESPACE, UUID.randomUUID().toString() + ); + final AllureExternalKey testKey = AllureExternalKey.of( + AllureTest.class, TEST_KEY_NAMESPACE, UUID.randomUUID().toString() + ); + lifecycle.registerScope(scopeKey); + lifecycle.startBeforeFixture(scopeKey, fixtureKey, new FixtureResult().setName("before")); + Allure.precondition("Scope precondition"); + Allure.preconditionHtml("

Scope precondition

"); + Allure.expectedResult("Scope expected result"); + Allure.expectedResultHtml("

Scope expected result

"); + lifecycle.stopFixture(fixtureKey); + + lifecycle.scheduleTest(List.of(scopeKey), testKey, test.setUuid(UUID.randomUUID().toString()).setName("test")); + lifecycle.startTest(testKey); + lifecycle.stopTest(testKey); + lifecycle.writeTest(testKey); + }); + } + @Test void shouldApplyRuntimeMetadataFromBeforeFixtureScope() { final String scopeUuid = UUID.randomUUID().toString(); diff --git a/allure-java-commons/src/test/java/io/qameta/allure/FileSystemResultsWriterTest.java b/allure-java-commons/src/test/java/io/qameta/allure/FileSystemResultsWriterTest.java index 55bcdd882..2b2f03026 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/FileSystemResultsWriterTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/FileSystemResultsWriterTest.java @@ -16,10 +16,12 @@ package io.qameta.allure; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.qameta.allure.internal.Allure2ModelJackson; import io.qameta.allure.model.GlobalAttachment; import io.qameta.allure.model.GlobalError; import io.qameta.allure.model.Globals; +import io.qameta.allure.model.ScopeResult; import io.qameta.allure.model.Status; import io.qameta.allure.model.StatusDetails; import io.qameta.allure.model.TestResult; @@ -86,6 +88,81 @@ void shouldWriteTestResult(@TempDir final Path folder) { .isRegularFile(); } + /** + * Result files preserve Markdown and HTML preconditions and expected results as separate JSON fields. + */ + @Description + @Test + void shouldWritePreconditionAndExpectedResult(@TempDir final Path folder) throws IOException { + final ObjectMapper mapper = Allure2ModelJackson.createMapper(); + final String uuid = UUID.randomUUID().toString(); + final TestResult result = new TestResult() + .setUuid(uuid) + .setPrecondition("User is **logged in**") + .setPreconditionHtml("

User is logged in

") + .setExpectedResult("**Dashboard** is displayed") + .setExpectedResultHtml("

Dashboard is displayed

"); + final FileSystemResultsWriter writer = new FileSystemResultsWriter(folder); + + writer.write(result); + + final JsonNode payload = mapper.readTree(folder.resolve(generateTestResultName(uuid)).toFile()); + assertThat(payload.path("precondition").asText()).isEqualTo(result.getPrecondition()); + assertThat(payload.path("preconditionHtml").asText()).isEqualTo(result.getPreconditionHtml()); + assertThat(payload.path("expectedResult").asText()).isEqualTo(result.getExpectedResult()); + assertThat(payload.path("expectedResultHtml").asText()).isEqualTo(result.getExpectedResultHtml()); + final TestResult restored = mapper.treeToValue(payload, TestResult.class); + assertThat(restored) + .usingRecursiveComparison() + .isEqualTo(result); + } + + /** + * Scope metadata can round-trip all four fields without conflating Markdown and HTML content. + */ + @Description + @Test + void shouldSerializeScopePreconditionAndExpectedResult() throws IOException { + final ObjectMapper mapper = Allure2ModelJackson.createMapper(); + final ScopeResult scope = new ScopeResult() + .setPrecondition("Scope precondition") + .setPreconditionHtml("

Scope precondition

") + .setExpectedResult("Scope expected result") + .setExpectedResultHtml("

Scope expected result

"); + + final String json = mapper.writeValueAsString(scope); + + final JsonNode payload = mapper.readTree(json); + assertThat(payload.path("precondition").asText()).isEqualTo(scope.getPrecondition()); + assertThat(payload.path("preconditionHtml").asText()).isEqualTo(scope.getPreconditionHtml()); + assertThat(payload.path("expectedResult").asText()).isEqualTo(scope.getExpectedResult()); + assertThat(payload.path("expectedResultHtml").asText()).isEqualTo(scope.getExpectedResultHtml()); + final ScopeResult restored = mapper.readValue(json, ScopeResult.class); + assertThat(restored) + .usingRecursiveComparison() + .isEqualTo(scope); + } + + /** + * Tests without preconditions or expected results do not emit null metadata fields. + */ + @Description + @Test + void shouldOmitUnsetPreconditionAndExpectedResult(@TempDir final Path folder) throws IOException { + final String uuid = UUID.randomUUID().toString(); + final TestResult result = new TestResult().setUuid(uuid); + final FileSystemResultsWriter writer = new FileSystemResultsWriter(folder); + + writer.write(result); + + final JsonNode payload = Allure2ModelJackson.createMapper() + .readTree(folder.resolve(generateTestResultName(uuid)).toFile()); + assertThat(payload.has("precondition")).isFalse(); + assertThat(payload.has("preconditionHtml")).isFalse(); + assertThat(payload.has("expectedResult")).isFalse(); + assertThat(payload.has("expectedResultHtml")).isFalse(); + } + @Test void shouldWriteTestResultContainer(@TempDir final Path folder) { FileSystemResultsWriter writer = new FileSystemResultsWriter(folder); diff --git a/allure-model/src/main/java/io/qameta/allure/model/ScopeResult.java b/allure-model/src/main/java/io/qameta/allure/model/ScopeResult.java index 31a3efe22..7231da5a7 100644 --- a/allure-model/src/main/java/io/qameta/allure/model/ScopeResult.java +++ b/allure-model/src/main/java/io/qameta/allure/model/ScopeResult.java @@ -39,6 +39,10 @@ public class ScopeResult implements Serializable, WithMetadata { private List parameters = new ArrayList<>(); private String description; private String descriptionHtml; + private String precondition; + private String preconditionHtml; + private String expectedResult; + private String expectedResultHtml; /** * Gets uuid. @@ -201,6 +205,86 @@ public ScopeResult setDescriptionHtml(final String value) { return this; } + /** + * Gets precondition. + * + * @return the precondition + */ + public String getPrecondition() { + return precondition; + } + + /** + * Sets precondition. + * + * @param value the precondition + * @return self for method chaining + */ + public ScopeResult setPrecondition(final String value) { + this.precondition = value; + return this; + } + + /** + * Gets HTML precondition. + * + * @return the HTML precondition + */ + public String getPreconditionHtml() { + return preconditionHtml; + } + + /** + * Sets HTML precondition. + * + * @param value the HTML precondition + * @return self for method chaining + */ + public ScopeResult setPreconditionHtml(final String value) { + this.preconditionHtml = value; + return this; + } + + /** + * Gets expected result. + * + * @return the expected result + */ + public String getExpectedResult() { + return expectedResult; + } + + /** + * Sets expected result. + * + * @param value the expected result + * @return self for method chaining + */ + public ScopeResult setExpectedResult(final String value) { + this.expectedResult = value; + return this; + } + + /** + * Gets HTML expected result. + * + * @return the HTML expected result + */ + public String getExpectedResultHtml() { + return expectedResultHtml; + } + + /** + * Sets HTML expected result. + * + * @param value the HTML expected result + * @return self for method chaining + */ + public ScopeResult setExpectedResultHtml(final String value) { + this.expectedResultHtml = value; + return this; + } + /** * {@inheritDoc} */ diff --git a/allure-model/src/main/java/io/qameta/allure/model/TestResult.java b/allure-model/src/main/java/io/qameta/allure/model/TestResult.java index 8d56e7b85..b8a5dd288 100644 --- a/allure-model/src/main/java/io/qameta/allure/model/TestResult.java +++ b/allure-model/src/main/java/io/qameta/allure/model/TestResult.java @@ -52,6 +52,10 @@ public class TestResult implements Serializable, ExecutableItem, WithMetadata { private Stage stage; private String description; private String descriptionHtml; + private String precondition; + private String preconditionHtml; + private String expectedResult; + private String expectedResultHtml; private List steps = new ArrayList<>(); private List attachments = new ArrayList<>(); private List parameters = new ArrayList<>(); @@ -341,6 +345,86 @@ public TestResult setDescriptionHtml(final String value) { return this; } + /** + * Gets precondition. + * + * @return the precondition + */ + public String getPrecondition() { + return precondition; + } + + /** + * Sets precondition. + * + * @param value the precondition + * @return self for method chaining + */ + public TestResult setPrecondition(final String value) { + this.precondition = value; + return this; + } + + /** + * Gets HTML precondition. + * + * @return the HTML precondition + */ + public String getPreconditionHtml() { + return preconditionHtml; + } + + /** + * Sets HTML precondition. + * + * @param value the HTML precondition + * @return self for method chaining + */ + public TestResult setPreconditionHtml(final String value) { + this.preconditionHtml = value; + return this; + } + + /** + * Gets expected result. + * + * @return the expected result + */ + public String getExpectedResult() { + return expectedResult; + } + + /** + * Sets expected result. + * + * @param value the expected result + * @return self for method chaining + */ + public TestResult setExpectedResult(final String value) { + this.expectedResult = value; + return this; + } + + /** + * Gets HTML expected result. + * + * @return the HTML expected result + */ + public String getExpectedResultHtml() { + return expectedResultHtml; + } + + /** + * Sets HTML expected result. + * + * @param value the HTML expected result + * @return self for method chaining + */ + public TestResult setExpectedResultHtml(final String value) { + this.expectedResultHtml = value; + return this; + } + /** * Gets steps. * diff --git a/allure-model/src/main/java/io/qameta/allure/model/WithMetadata.java b/allure-model/src/main/java/io/qameta/allure/model/WithMetadata.java index 64cc9f98d..41906a7cf 100644 --- a/allure-model/src/main/java/io/qameta/allure/model/WithMetadata.java +++ b/allure-model/src/main/java/io/qameta/allure/model/WithMetadata.java @@ -18,7 +18,8 @@ import java.util.List; /** - * The interface for model objects that carry test metadata: labels, links, parameters, and description. + * The interface for model objects that carry test metadata: labels, links, parameters, description, + * precondition, and expected result. * *

Shared by {@link TestResult} and {@link ScopeResult} so runtime APIs can apply metadata to whichever is the * current target without branching on the concrete type.

@@ -66,4 +67,64 @@ public interface WithMetadata extends WithLinks, WithParameters { */ WithMetadata setDescriptionHtml(String value); + /** + * Gets precondition. + * + * @return the precondition + */ + String getPrecondition(); + + /** + * Sets precondition. + * + * @param value the precondition + * @return self for chaining + */ + WithMetadata setPrecondition(String value); + + /** + * Gets HTML precondition. + * + * @return the HTML precondition + */ + String getPreconditionHtml(); + + /** + * Sets HTML precondition. + * + * @param value the HTML precondition + * @return self for chaining + */ + WithMetadata setPreconditionHtml(String value); + + /** + * Gets expected result. + * + * @return the expected result + */ + String getExpectedResult(); + + /** + * Sets expected result. + * + * @param value the expected result + * @return self for chaining + */ + WithMetadata setExpectedResult(String value); + + /** + * Gets HTML expected result. + * + * @return the HTML expected result + */ + String getExpectedResultHtml(); + + /** + * Sets HTML expected result. + * + * @param value the HTML expected result + * @return self for chaining + */ + WithMetadata setExpectedResultHtml(String value); + }