From 9adaf9b6cd3cece38866219738c60b5152defd78 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sat, 19 Jul 2025 13:28:23 +0200 Subject: [PATCH 01/14] refactor: screenshot emitted as event payload to optimize video generation --- .../giulong/spectrum/SpectrumEntity.java | 22 +- .../interceptors/SpectrumInterceptor.java | 9 +- .../extensions/resolvers/DriverResolver.java | 5 +- .../resolvers/TestContextResolver.java | 7 +- .../extensions/watchers/EventsWatcher.java | 17 +- .../giulong/spectrum/pojos/Screenshot.java | 17 + .../giulong/spectrum/pojos/events/Event.java | 9 +- .../giulong/spectrum/types/TestData.java | 15 + .../giulong/spectrum/utils/FileUtils.java | 5 +- .../giulong/spectrum/utils/HtmlUtils.java | 24 + .../spectrum/utils/events/EventsConsumer.java | 5 + .../utils/events/EventsDispatcher.java | 18 +- .../spectrum/utils/events/VideoConsumer.java | 152 ----- .../utils/events/VideoDynamicConsumer.java | 48 -- .../utils/events/video/VideoBaseConsumer.java | 24 + .../utils/events/video/VideoConsumer.java | 113 ++++ .../events/video/VideoDynamicConsumer.java | 52 ++ .../events/video/VideoDynamicFinalizer.java | 16 + .../video/VideoDynamicInitConsumer.java | 16 + .../utils/events/video/VideoFinalizer.java | 42 ++ .../utils/events/video/VideoInitConsumer.java | 30 + .../web_driver_events/ScreenshotConsumer.java | 31 +- .../resources/yaml/configuration.default.yaml | 30 +- .../giulong/spectrum/SpectrumEntityTest.java | 62 ++- .../interceptors/SpectrumInterceptorTest.java | 5 + .../resolvers/DriverResolverTest.java | 4 +- .../resolvers/TestContextResolverTest.java | 10 +- .../watchers/EventsWatcherTest.java | 56 ++ .../giulong/spectrum/utils/FileUtilsTest.java | 13 +- .../giulong/spectrum/utils/HtmlUtilsTest.java | 77 ++- .../utils/events/EventsDispatcherTest.java | 38 +- .../utils/events/VideoConsumerTest.java | 518 ------------------ .../events/VideoDynamicConsumerTest.java | 141 ----- .../events/video/VideoBaseConsumerTest.java | 87 +++ .../utils/events/video/VideoConsumerTest.java | 389 +++++++++++++ .../video/VideoDynamicConsumerTest.java | 191 +++++++ .../video/VideoDynamicFinalizerTest.java | 32 ++ .../video/VideoDynamicInitConsumerTest.java | 32 ++ .../events/video/VideoFinalizerTest.java | 108 ++++ .../events/video/VideoInitConsumerTest.java | 75 +++ .../ScreenshotConsumerTest.java | 64 +-- 41 files changed, 1596 insertions(+), 1013 deletions(-) create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java delete mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoConsumer.java delete mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoConsumer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizer.java create mode 100644 spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumer.java delete mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoConsumerTest.java delete mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoConsumerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizerTest.java create mode 100644 spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumerTest.java diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java b/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java index acc41d2b..dd748639 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java @@ -5,6 +5,7 @@ import com.aventstack.extentreports.Status; import com.aventstack.extentreports.model.Media; import io.github.giulong.spectrum.interfaces.Shared; +import io.github.giulong.spectrum.pojos.Screenshot; import io.github.giulong.spectrum.types.TestData; import io.github.giulong.spectrum.utils.*; import io.github.giulong.spectrum.utils.events.EventsDispatcher; @@ -13,8 +14,8 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.datafaker.Faker; +import org.junit.jupiter.api.extension.ExtensionContext; import org.openqa.selenium.By; -import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.bidi.browsingcontext.BrowsingContext; @@ -28,11 +29,13 @@ import java.nio.file.Path; import java.security.MessageDigest; import java.util.Arrays; +import java.util.Map; import static com.aventstack.extentreports.MediaEntityBuilder.createScreenCaptureFromPath; import static com.aventstack.extentreports.Status.*; import static io.github.giulong.spectrum.enums.Frame.MANUAL; -import static org.openqa.selenium.OutputType.BYTES; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.pojos.Screenshot.SCREENSHOT; @Slf4j public abstract class SpectrumEntity, Data> { @@ -178,23 +181,24 @@ public T screenshotFail(final String msg) { */ @SneakyThrows public Media addScreenshotToReport(final String msg, final Status status) { - final String fileName = fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest); - final Path screenshotPath = testData.getScreenshotFolderPath().resolve(fileName); + final ExtensionContext context = testContext.get(EXTENSION_CONTEXT, ExtensionContext.class); + final Screenshot screenshot = htmlUtils.buildScreenshotFrom(context); - Files.write(screenshotPath, ((TakesScreenshot) driver).getScreenshotAs(BYTES)); + eventsDispatcher.fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); - final Media screenshot = createScreenCaptureFromPath(screenshotPath.toString()).build(); + Files.write(screenshot.getPath(), screenshot.getData()); + final Media media = createScreenCaptureFromPath(screenshot.getPath().toString()).build(); if (msg == null) { - statefulExtentTest.getCurrentNode().log(status, (String) null, screenshot); + statefulExtentTest.getCurrentNode().log(status, (String) null, media); } else { final int frameNumber = configuration.getVideo().getAndIncrementFrameNumberFor(testData, MANUAL); final String tag = htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message"); - statefulExtentTest.getCurrentNode().log(status, tag, screenshot); + statefulExtentTest.getCurrentNode().log(status, tag, media); } - return screenshot; + return media; } /** diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptor.java b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptor.java index 2ff8ca71..e7ef1b8e 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptor.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptor.java @@ -43,7 +43,9 @@ public void interceptDynamicTest(final Invocation invocation, final Dynami final String testName = context.getDisplayName(); final Path dynamicVideoPath = Path.of(String.format("%s-%s.mp4", fileUtils.removeExtensionFrom(testData.getVideoPath().toString()), testName)); final ExtentTest currentNode = statefulExtentTest.createNode(testName); + final Set tags = Set.of(DYNAMIC_TEST); + testData.setDynamic(true); testData.setFrameNumber(0); testData.setDisplayName(testName); testData.setDynamicVideoPath(dynamicVideoPath); @@ -56,12 +58,13 @@ public void interceptDynamicTest(final Invocation invocation, final Dynami } try { - eventsDispatcher.fire(className, testName, BEFORE, null, Set.of(DYNAMIC_TEST), context); + eventsDispatcher.fire(className, testName, BEFORE_EXECUTION, null, tags, context); + eventsDispatcher.fire(className, testName, BEFORE, null, tags, context); invocation.proceed(); - eventsDispatcher.fire(className, testName, AFTER, SUCCESSFUL, Set.of(DYNAMIC_TEST), context); + eventsDispatcher.fire(className, testName, AFTER, SUCCESSFUL, tags, context); } catch (Throwable t) { currentNode.fail(t); - eventsDispatcher.fire(className, testName, AFTER, FAILED, Set.of(DYNAMIC_TEST), context); + eventsDispatcher.fire(className, testName, AFTER, FAILED, tags, context); throw t; } finally { diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolver.java b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolver.java index 1e7c1fb8..6b5af3cf 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolver.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolver.java @@ -14,7 +14,6 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.support.TypeBasedParameterResolver; import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.decorators.Decorated; @@ -73,10 +72,8 @@ public WebDriver resolveParameter(final ParameterContext arg0, final ExtensionCo final ScreenshotConsumer screenshotConsumer = ScreenshotConsumer .builder() .enabled(true) - .driver((TakesScreenshot) driver) - .statefulExtentTest(statefulExtentTest) - .testData(testData) .video(video) + .context(context) .build(); final TestStepBuilderConsumer testStepBuilderConsumer = TestStepBuilderConsumer diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolver.java b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolver.java index ea852b96..3efaad5d 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolver.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolver.java @@ -1,18 +1,17 @@ package io.github.giulong.spectrum.extensions.resolvers; -import io.github.giulong.spectrum.utils.TestContext; import io.github.giulong.spectrum.utils.ContextManager; +import io.github.giulong.spectrum.utils.TestContext; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.support.TypeBasedParameterResolver; -import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; - @Slf4j public class TestContextResolver extends TypeBasedParameterResolver { public static final String TEST_CONTEXT = "testContext"; + public static final String EXTENSION_CONTEXT = "extensionContext"; private final ContextManager contextManager = ContextManager.getInstance(); @@ -21,7 +20,7 @@ public TestContext resolveParameter(final ParameterContext arg0, final Extension log.debug("Resolving {}", TEST_CONTEXT); final TestContext testContext = contextManager.initFor(context); - context.getStore(GLOBAL).put(TEST_CONTEXT, testContext); + testContext.put(EXTENSION_CONTEXT, context); return testContext; } diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcher.java b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcher.java index dd2ed036..228b8ec4 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcher.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcher.java @@ -11,7 +11,7 @@ import static io.github.giulong.spectrum.enums.Result.*; import static io.github.giulong.spectrum.utils.events.EventsDispatcher.*; -public class EventsWatcher implements TestWatcher, BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback { +public class EventsWatcher implements TestWatcher, BeforeTestExecutionCallback, BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback { private final EventsDispatcher eventsDispatcher = EventsDispatcher.getInstance(); @@ -25,9 +25,18 @@ public void beforeEach(final ExtensionContext context) { notifyTest(context, BEFORE, null, Set.of(TEST)); } + @Override + public void beforeTestExecution(final ExtensionContext context) { + notifyTest(context, BEFORE_EXECUTION, null, Set.of(TEST)); + + if (isTestFactory(context)) { + notifyTest(context, BEFORE_EXECUTION, null, Set.of(TEST_FACTORY)); + } + } + @Override public void afterEach(final ExtensionContext context) { - if (context.getRequiredTestMethod().isAnnotationPresent(TestFactory.class)) { + if (isTestFactory(context)) { notifyTest(context, AFTER, SUCCESSFUL, Set.of(TEST_FACTORY)); } } @@ -57,6 +66,10 @@ public void testFailed(final ExtensionContext context, final Throwable exception notifyTest(context, AFTER, FAILED, Set.of(TEST)); } + boolean isTestFactory(final ExtensionContext context) { + return context.getRequiredTestMethod().isAnnotationPresent(TestFactory.class); + } + void notifyClass(final ExtensionContext context, final String reason, final Result result, final Set tags) { final String className = context.getDisplayName(); diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java b/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java new file mode 100644 index 00000000..1f6eb061 --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java @@ -0,0 +1,17 @@ +package io.github.giulong.spectrum.pojos; + +import lombok.Builder; +import lombok.Getter; + +import java.nio.file.Path; + +@Getter +@Builder +public class Screenshot { + + public static final String SCREENSHOT = "screenshot"; + + private String name; + private Path path; + private byte[] data; +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/pojos/events/Event.java b/spectrum/src/main/java/io/github/giulong/spectrum/pojos/events/Event.java index b4bf89b2..0873e55b 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/pojos/events/Event.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/pojos/events/Event.java @@ -2,10 +2,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import io.github.giulong.spectrum.enums.Result; -import lombok.*; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; import lombok.extern.jackson.Jacksonized; import org.junit.jupiter.api.extension.ExtensionContext; +import java.util.Map; import java.util.Set; @Getter @@ -23,4 +26,8 @@ public class Event { @JsonIgnore @ToString.Exclude private ExtensionContext context; + + @JsonIgnore + @ToString.Exclude + private Map payload; } diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java b/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java index 9481e4e2..a0f35ceb 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java @@ -3,8 +3,11 @@ import lombok.Builder; import lombok.Getter; import lombok.Setter; +import org.jcodec.api.awt.AWTSequenceEncoder; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; @Getter @Builder @@ -17,9 +20,21 @@ public class TestData { private Path screenshotFolderPath; private Path videoPath; + @Builder.Default + private Map encoders = new HashMap<>(); + @Setter private int frameNumber; + @Setter + private boolean dynamic; + + @Setter + private byte[] lastFrameDigest; + + @Setter + private String lastFrameDisplayName; + @Setter private Path dynamicVideoPath; diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java index 558414ab..95abffaa 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java @@ -1,6 +1,5 @@ package io.github.giulong.spectrum.utils; -import io.github.giulong.spectrum.enums.Frame; import lombok.NoArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -130,7 +129,7 @@ public FileTime getCreationTimeOf(final File file) { return Files.readAttributes(file.toPath(), BasicFileAttributes.class).creationTime(); } - public String getScreenshotNameFrom(final Frame frame, final StatefulExtentTest statefulExtentTest) { - return String.format("%s-%s-%s.png", frame.getValue(), statefulExtentTest.getDisplayName(), randomUUID()); + public String buildScreenshotNameFrom(final StatefulExtentTest statefulExtentTest) { + return String.format("%s-%s.png", statefulExtentTest.getDisplayName(), randomUUID()); } } diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java index 7da5db6e..324db93b 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java @@ -1,10 +1,14 @@ package io.github.giulong.spectrum.utils; import io.github.giulong.spectrum.interfaces.SessionHook; +import io.github.giulong.spectrum.pojos.Screenshot; import io.github.giulong.spectrum.types.TestData; import lombok.NoArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; import java.nio.file.Files; import java.nio.file.Path; @@ -13,8 +17,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.StatefulExtentTestResolver.STATEFUL_EXTENT_TEST; +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; import static java.util.regex.Pattern.DOTALL; import static lombok.AccessLevel.PRIVATE; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; +import static org.openqa.selenium.OutputType.BYTES; @Slf4j @NoArgsConstructor(access = PRIVATE) @@ -56,6 +65,21 @@ public String buildFrameTagFor(final int number, final String content, final Tes return freeMarkerWrapper.interpolate(this.frameTemplate, Map.of("classes", classes, "id", testData.getTestId(), "number", number, "content", content)); } + public Screenshot buildScreenshotFrom(final ExtensionContext context) { + final ExtensionContext.Store store = context.getStore(GLOBAL); + final StatefulExtentTest statefulExtentTest = store.get(STATEFUL_EXTENT_TEST, StatefulExtentTest.class); + final String fileName = fileUtils.buildScreenshotNameFrom(statefulExtentTest); + final TestData testData = store.get(TEST_DATA, TestData.class); + final TakesScreenshot driver = (TakesScreenshot) store.get(DRIVER, WebDriver.class); + + return Screenshot + .builder() + .name(fileName) + .path(testData.getScreenshotFolderPath().resolve(fileName)) + .data(driver.getScreenshotAs(BYTES)) + .build(); + } + @SneakyThrows public String inlineImagesOf(final String html) { final Matcher matcher = IMAGE_TAG.matcher(html); diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsConsumer.java index d8ca9d71..05cf8467 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsConsumer.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsConsumer.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.utils.events.video.*; import io.github.giulong.spectrum.utils.web_driver_events.TestStepsConsumer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -22,8 +23,12 @@ @JsonSubTypes.Type(value = ExtentTestConsumer.class, name = "extentTest"), @JsonSubTypes.Type(value = DriverConsumer.class, name = "driver"), @JsonSubTypes.Type(value = MailConsumer.class, name = "mail"), + @JsonSubTypes.Type(value = VideoInitConsumer.class, name = "videoInit"), + @JsonSubTypes.Type(value = VideoDynamicInitConsumer.class, name = "videoDynamicInit"), @JsonSubTypes.Type(value = VideoConsumer.class, name = "video"), @JsonSubTypes.Type(value = VideoDynamicConsumer.class, name = "videoDynamic"), + @JsonSubTypes.Type(value = VideoFinalizer.class, name = "videoFinalizer"), + @JsonSubTypes.Type(value = VideoDynamicFinalizer.class, name = "videoDynamicFinalizer"), @JsonSubTypes.Type(value = TestStepsConsumer.class, name = "testSteps"), @JsonSubTypes.Type(value = LogConsumer.class, name = "log"), }) diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsDispatcher.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsDispatcher.java index 3e325354..8b683cbd 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsDispatcher.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/EventsDispatcher.java @@ -2,12 +2,14 @@ import io.github.giulong.spectrum.enums.Result; import io.github.giulong.spectrum.interfaces.SessionHook; -import io.github.giulong.spectrum.utils.Configuration; import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.utils.Configuration; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.extension.ExtensionContext; +import java.util.Map; +import java.util.Optional; import java.util.Set; import static lombok.AccessLevel.PRIVATE; @@ -19,6 +21,7 @@ public class EventsDispatcher implements SessionHook { private static final EventsDispatcher INSTANCE = new EventsDispatcher(); public static final String BEFORE = "before"; + public static final String BEFORE_EXECUTION = "beforeExecution"; public static final String AFTER = "after"; public static final String TEST = "test"; public static final String TEST_FACTORY = "testFactory"; @@ -52,6 +55,10 @@ public void fire(final String reason, final Set tags, final Result resul fire(null, null, reason, result, tags, null); } + public void fire(final String primaryId, final String reason, final Map payload) { + fire(primaryId, null, reason, null, null, null, payload); + } + public void fire(final String primaryId, final String reason) { fire(primaryId, null, reason, null, null, null); } @@ -60,7 +67,13 @@ public void fire(final String primaryId, final String secondaryId, final String fire(primaryId, secondaryId, reason, null, null, null); } - public void fire(final String primaryId, final String secondaryId, final String reason, final Result result, final Set tags, final ExtensionContext context) { + public void fire(final String primaryId, final String secondaryId, final String reason, final Result result, final Set tags, + final ExtensionContext context) { + fire(primaryId, secondaryId, reason, result, tags, context, null); + } + + public void fire(final String primaryId, final String secondaryId, final String reason, final Result result, final Set tags, + final ExtensionContext context, final Map payload) { final Event event = Event.builder() .primaryId(primaryId) .secondaryId(secondaryId) @@ -68,6 +81,7 @@ public void fire(final String primaryId, final String secondaryId, final String .result(result) .tags(tags) .context(context) + .payload(Optional.ofNullable(payload).orElseGet(Map::of)) .build(); log.debug("Dispatching event {}", event); diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoConsumer.java deleted file mode 100644 index d048cf3d..00000000 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoConsumer.java +++ /dev/null @@ -1,152 +0,0 @@ -package io.github.giulong.spectrum.utils.events; - -import com.fasterxml.jackson.annotation.JsonView; -import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; -import io.github.giulong.spectrum.pojos.events.Event; -import io.github.giulong.spectrum.types.TestData; -import io.github.giulong.spectrum.utils.Configuration; -import io.github.giulong.spectrum.utils.ContextManager; -import io.github.giulong.spectrum.utils.video.Video; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.jcodec.api.awt.AWTSequenceEncoder; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.WebDriver; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.stream.Stream; - -import static io.github.giulong.spectrum.SpectrumEntity.HASH_ALGORITHM; -import static io.github.giulong.spectrum.enums.Result.DISABLED; -import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.ORIGINAL_DRIVER; -import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; -import static java.awt.image.BufferedImage.TYPE_INT_RGB; -import static java.util.Comparator.comparingLong; - -@Slf4j -@JsonView(Internal.class) -public class VideoConsumer extends EventsConsumer { - - private final ClassLoader classLoader = VideoConsumer.class.getClassLoader(); - private final Configuration configuration = Configuration.getInstance(); - private final ContextManager contextManager = ContextManager.getInstance(); - - private byte[] lastFrameDigest; - private MessageDigest messageDigest; - - @Override - protected boolean shouldAccept(final Event event) { - return !DISABLED.equals(event.getResult()) && !configuration.getVideo().isDisabled(); - } - - @SneakyThrows - @Override - public void accept(final Event event) { - final Video video = configuration.getVideo(); - final ExtensionContext context = event.getContext(); - final TestData testData = contextManager.get(context, TEST_DATA, TestData.class); - - init(); - - log.info("Generating video for test {}.{}", testData.getClassName(), testData.getMethodName()); - - try (Stream screenshots = Files.walk(testData.getScreenshotFolderPath())) { - final AWTSequenceEncoder encoder = AWTSequenceEncoder.createSequenceEncoder(getVideoPathFrom(testData).toFile(), 1); - final List frames = screenshots - .map(Path::toFile) - .filter(File::isFile) - .filter(file -> filter(file, testData)) - .filter(file -> !video.isSkipDuplicateFrames() || isNewFrame(file, testData)) - .sorted(comparingLong(File::lastModified)) - .toList(); - - if (frames.isEmpty()) { - log.debug("No frames were added to the video. Adding 'no-video.png'"); - final URL noVideoPng = Objects.requireNonNull(classLoader.getResource("no-video.png")); - encoder.encodeImage(ImageIO.read(noVideoPng)); - } else { - final Dimension dimension = chooseDimensionFor(contextManager.get(context, ORIGINAL_DRIVER, WebDriver.class), video); - - for (File frame : frames) { - encoder.encodeImage(resize(ImageIO.read(frame), dimension)); - } - } - - encoder.finish(); - } - } - - @SneakyThrows - protected void init() { - this.lastFrameDigest = null; - this.messageDigest = MessageDigest.getInstance(HASH_ALGORITHM); - } - - protected Path getVideoPathFrom(final TestData testData) { - return testData.getVideoPath(); - } - - protected boolean filter(final File file, final TestData testData) { - return true; - } - - @SneakyThrows - protected boolean isNewFrame(final File screenshot, final TestData testData) { - final byte[] digest = messageDigest.digest(Files.readAllBytes(screenshot.toPath())); - - if (!Arrays.equals(lastFrameDigest, digest)) { - lastFrameDigest = digest; - return true; - } - - log.trace("Discarding duplicate frame {}", screenshot.getName()); - return false; - } - - Dimension chooseDimensionFor(final WebDriver driver, final Video video) { - int width = video.getWidth(); - int height = video.getHeight(); - - if (video.getWidth() < 1 || video.getHeight() < 1) { - final Dimension size = driver.manage().window().getSize(); - width = size.getWidth(); - height = size.getHeight() - video.getMenuBarsHeight(); - } - - final int evenWidth = makeItEven(width); - final int evenHeight = makeItEven(height); - - log.debug("Video dimensions: {}x{}", evenWidth, evenHeight); - return new Dimension(evenWidth, evenHeight); - } - - int makeItEven(final int i) { - return i % 2 == 0 ? i : i + 1; - } - - BufferedImage resize(final BufferedImage bufferedImage, final Dimension dimension) { - final int width = dimension.getWidth(); - final int height = dimension.getHeight(); - final int minWidth = Math.min(width, bufferedImage.getWidth()); - final int minHeight = Math.min(height, bufferedImage.getHeight()); - final BufferedImage resizedImage = new BufferedImage(width, height, TYPE_INT_RGB); - final Graphics2D graphics2D = resizedImage.createGraphics(); - - log.trace("Resizing screenshot to {}x{}", minWidth, minHeight); - graphics2D.drawImage(bufferedImage, 0, 0, minWidth, minHeight, null); - graphics2D.dispose(); - - return resizedImage; - } -} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumer.java deleted file mode 100644 index 553e41fe..00000000 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumer.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.giulong.spectrum.utils.events; - -import com.fasterxml.jackson.annotation.JsonView; -import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; -import io.github.giulong.spectrum.types.TestData; - -import java.io.File; -import java.nio.file.Path; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@JsonView(Internal.class) -public class VideoDynamicConsumer extends VideoConsumer { - - private static final Pattern PATTERN = Pattern.compile("[A-Za-z]+-(?.*)-([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})\\.png"); - - private String lastFrameDisplayName; - - @Override - protected void init() { - super.init(); - this.lastFrameDisplayName = null; - } - - @Override - protected Path getVideoPathFrom(final TestData testData) { - return testData.getDynamicVideoPath(); - } - - @Override - protected boolean filter(final File file, final TestData testData) { - final Matcher matcher = PATTERN.matcher(file.getName()); - - return matcher.find() && testData.getDisplayName().equals(matcher.group("displayName")); - } - - @Override - protected boolean isNewFrame(final File screenshot, final TestData testData) { - final String displayName = testData.getDisplayName(); - - if (!displayName.equals(lastFrameDisplayName)) { - lastFrameDisplayName = displayName; - return true; - } - - return super.isNewFrame(screenshot, testData); - } -} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumer.java new file mode 100644 index 00000000..8379100e --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumer.java @@ -0,0 +1,24 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import io.github.giulong.spectrum.utils.Configuration; +import io.github.giulong.spectrum.utils.events.EventsConsumer; + +import java.nio.file.Path; + +import static io.github.giulong.spectrum.enums.Result.DISABLED; + +public abstract class VideoBaseConsumer extends EventsConsumer { + + protected final Configuration configuration = Configuration.getInstance(); + + @Override + protected boolean shouldAccept(final Event event) { + return !DISABLED.equals(event.getResult()) && !configuration.getVideo().isDisabled(); + } + + protected Path getVideoPathFrom(final TestData testData) { + return testData.getVideoPath(); + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoConsumer.java new file mode 100644 index 00000000..a7ef1706 --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoConsumer.java @@ -0,0 +1,113 @@ +package io.github.giulong.spectrum.utils.events.video; + +import com.fasterxml.jackson.annotation.JsonView; +import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; +import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import io.github.giulong.spectrum.utils.video.Video; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jcodec.api.awt.AWTSequenceEncoder; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.WebDriver; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Map; + +import static io.github.giulong.spectrum.SpectrumEntity.HASH_ALGORITHM; +import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.ORIGINAL_DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static io.github.giulong.spectrum.pojos.Screenshot.SCREENSHOT; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; + +@Slf4j +@JsonView(Internal.class) +public class VideoConsumer extends VideoBaseConsumer { + + private final MessageDigest messageDigest; + + @SneakyThrows + public VideoConsumer() { + this.messageDigest = MessageDigest.getInstance(HASH_ALGORITHM); + } + + @Override + @SneakyThrows + public void accept(final Event event) { + final Video video = configuration.getVideo(); + final Map payload = event.getPayload(); + final ExtensionContext.Store store = ((ExtensionContext) payload.get(EXTENSION_CONTEXT)).getStore(GLOBAL); + final TestData testData = store.get(TEST_DATA, TestData.class); + final Screenshot screenshot = (Screenshot) payload.get(SCREENSHOT); + + if (video.isSkipDuplicateFrames() && !isNewFrame(screenshot, testData)) { + return; + } + + final Path videoPath = getVideoPathFrom(testData); + final AWTSequenceEncoder encoder = testData.getEncoders().get(videoPath); + final Dimension dimension = chooseDimensionFor(store.get(ORIGINAL_DRIVER, WebDriver.class), video); + + log.debug("Adding screenshot to video {}", videoPath.getFileName()); + encoder.encodeImage(resize(ImageIO.read(new ByteArrayInputStream(screenshot.getData())), dimension)); + } + + @SneakyThrows + protected boolean isNewFrame(final Screenshot screenshot, final TestData testData) { + final byte[] digest = messageDigest.digest(screenshot.getData()); + + if (!Arrays.equals(testData.getLastFrameDigest(), digest)) { + testData.setLastFrameDigest(digest); + return true; + } + + log.trace("Discarding duplicate frame {}", screenshot.getName()); + return false; + } + + Dimension chooseDimensionFor(final WebDriver driver, final Video video) { + int width = video.getWidth(); + int height = video.getHeight(); + + if (video.getWidth() < 1 || video.getHeight() < 1) { + final Dimension size = driver.manage().window().getSize(); + width = size.getWidth(); + height = size.getHeight() - video.getMenuBarsHeight(); + } + + final int evenWidth = makeItEven(width); + final int evenHeight = makeItEven(height); + + log.debug("Video dimensions: {}x{}", evenWidth, evenHeight); + return new Dimension(evenWidth, evenHeight); + } + + int makeItEven(final int i) { + return i % 2 == 0 ? i : i + 1; + } + + BufferedImage resize(final BufferedImage bufferedImage, final Dimension dimension) { + final int width = dimension.getWidth(); + final int height = dimension.getHeight(); + final int minWidth = Math.min(width, bufferedImage.getWidth()); + final int minHeight = Math.min(height, bufferedImage.getHeight()); + final BufferedImage resizedImage = new BufferedImage(width, height, TYPE_INT_RGB); + final Graphics2D graphics2D = resizedImage.createGraphics(); + + log.trace("Resizing screenshot to {}x{}", minWidth, minHeight); + graphics2D.drawImage(bufferedImage, 0, 0, minWidth, minHeight, null); + graphics2D.dispose(); + + return resizedImage; + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumer.java new file mode 100644 index 00000000..7b4121de --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumer.java @@ -0,0 +1,52 @@ +package io.github.giulong.spectrum.utils.events.video; + +import com.fasterxml.jackson.annotation.JsonView; +import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; +import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.nio.file.Path; + +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; + +@JsonView(Internal.class) +public class VideoDynamicConsumer extends VideoConsumer { + + @Override + protected boolean shouldAccept(final Event event) { + final boolean superCondition = super.shouldAccept(event); + if (!superCondition) { + return false; + } + + final ExtensionContext context = (ExtensionContext) event.getPayload().get(EXTENSION_CONTEXT); + if (context == null) { + return false; + } + + final TestData testData = context.getStore(GLOBAL).get(TEST_DATA, TestData.class); + + return testData != null && testData.isDynamic(); + } + + @Override + protected Path getVideoPathFrom(final TestData testData) { + return testData.getDynamicVideoPath(); + } + + @Override + protected boolean isNewFrame(final Screenshot screenshot, final TestData testData) { + final String displayName = testData.getDisplayName(); + + if (!displayName.equals(testData.getLastFrameDisplayName())) { + testData.setLastFrameDisplayName(displayName); + return true; + } + + return super.isNewFrame(screenshot, testData); + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizer.java new file mode 100644 index 00000000..ab29a9e1 --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizer.java @@ -0,0 +1,16 @@ +package io.github.giulong.spectrum.utils.events.video; + +import com.fasterxml.jackson.annotation.JsonView; +import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; +import io.github.giulong.spectrum.types.TestData; + +import java.nio.file.Path; + +@JsonView(Internal.class) +public class VideoDynamicFinalizer extends VideoFinalizer { + + @Override + protected Path getVideoPathFrom(final TestData testData) { + return testData.getDynamicVideoPath(); + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumer.java new file mode 100644 index 00000000..fb08656c --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumer.java @@ -0,0 +1,16 @@ +package io.github.giulong.spectrum.utils.events.video; + +import com.fasterxml.jackson.annotation.JsonView; +import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; +import io.github.giulong.spectrum.types.TestData; + +import java.nio.file.Path; + +@JsonView(Internal.class) +public class VideoDynamicInitConsumer extends VideoInitConsumer { + + @Override + protected Path getVideoPathFrom(final TestData testData) { + return testData.getDynamicVideoPath(); + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizer.java new file mode 100644 index 00000000..dddce197 --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizer.java @@ -0,0 +1,42 @@ +package io.github.giulong.spectrum.utils.events.video; + +import com.fasterxml.jackson.annotation.JsonView; +import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jcodec.api.awt.AWTSequenceEncoder; + +import javax.imageio.ImageIO; +import java.net.URL; +import java.nio.file.Path; +import java.util.Objects; + +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; + +@Slf4j +@JsonView(Internal.class) +public class VideoFinalizer extends VideoBaseConsumer { + + private final ClassLoader classLoader = VideoFinalizer.class.getClassLoader(); + + @Override + @SneakyThrows + public void accept(final Event event) { + final TestData testData = event.getContext().getStore(GLOBAL).get(TEST_DATA, TestData.class); + final Path videoPath = getVideoPathFrom(testData); + final AWTSequenceEncoder encoder = testData.getEncoders().get(videoPath); + + log.debug("Finalizing video {}", videoPath.getFileName()); + + if (testData.getFrameNumber() == 0) { + log.debug("No frames were added to the video. Adding 'no-video.png'"); + final URL noVideoPng = Objects.requireNonNull(classLoader.getResource("no-video.png")); + encoder.encodeImage(ImageIO.read(noVideoPng)); + } + + encoder.finish(); + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumer.java new file mode 100644 index 00000000..7ec2b513 --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumer.java @@ -0,0 +1,30 @@ +package io.github.giulong.spectrum.utils.events.video; + +import com.fasterxml.jackson.annotation.JsonView; +import io.github.giulong.spectrum.internals.jackson.views.Views.Internal; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.nio.file.Path; + +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static org.jcodec.api.awt.AWTSequenceEncoder.createSequenceEncoder; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; + +@Slf4j +@JsonView(Internal.class) +public class VideoInitConsumer extends VideoBaseConsumer { + + @Override + @SneakyThrows + public void accept(final Event event) { + final TestData testData = event.getContext().getStore(GLOBAL).get(TEST_DATA, TestData.class); + final Path videoPath = getVideoPathFrom(testData); + + log.info("Generating video for test {}", videoPath.getFileName()); + + testData.getEncoders().put(videoPath, createSequenceEncoder(videoPath.toFile(), 1)); + } +} diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java index 31a92d27..bf5c4faa 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java @@ -1,41 +1,38 @@ package io.github.giulong.spectrum.utils.web_driver_events; import io.github.giulong.spectrum.enums.Frame; -import io.github.giulong.spectrum.types.TestData; -import io.github.giulong.spectrum.utils.FileUtils; -import io.github.giulong.spectrum.utils.StatefulExtentTest; +import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.utils.HtmlUtils; +import io.github.giulong.spectrum.utils.events.EventsDispatcher; import io.github.giulong.spectrum.utils.video.Video; -import lombok.SneakyThrows; import lombok.experimental.SuperBuilder; import lombok.extern.slf4j.Slf4j; -import org.openqa.selenium.TakesScreenshot; +import org.junit.jupiter.api.extension.ExtensionContext; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Map; -import static org.openqa.selenium.OutputType.BYTES; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.pojos.Screenshot.SCREENSHOT; @Slf4j @SuperBuilder public class ScreenshotConsumer extends WebDriverEventConsumer { - private final FileUtils fileUtils = FileUtils.getInstance(); + private final HtmlUtils htmlUtils = HtmlUtils.getInstance(); + private final EventsDispatcher eventsDispatcher = EventsDispatcher.getInstance(); - private TakesScreenshot driver; - private StatefulExtentTest statefulExtentTest; - private TestData testData; private Video video; + private ExtensionContext context; - @SneakyThrows @Override public void accept(final WebDriverEvent webDriverEvent) { final Frame frame = webDriverEvent.getFrame(); if (video.shouldRecord(frame)) { - final String fileName = fileUtils.getScreenshotNameFrom(frame, statefulExtentTest); - final Path screenshotPath = testData.getScreenshotFolderPath().resolve(fileName); - final Path path = Files.write(screenshotPath, driver.getScreenshotAs(BYTES)); - log.trace("Recording frame {} for event '{}' at {}", frame, webDriverEvent.getMessage(), path); + final Screenshot screenshot = htmlUtils.buildScreenshotFrom(context); + + eventsDispatcher.fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); + log.trace("Recording frame {} for event '{}'", frame, webDriverEvent.getMessage()); return; } diff --git a/spectrum/src/main/resources/yaml/configuration.default.yaml b/spectrum/src/main/resources/yaml/configuration.default.yaml index c02405ed..fe2d6f23 100644 --- a/spectrum/src/main/resources/yaml/configuration.default.yaml +++ b/spectrum/src/main/resources/yaml/configuration.default.yaml @@ -476,23 +476,39 @@ faker: # Internal events consumers. Cannot be removed, modified, nor added from client side configuration*.yaml, as lists' elements are appended when merging yaml files. You can add yours eventsConsumers: - - extentTest: # We need to add an entry to the Extent Report once each test is done + - videoInit: # initialize the video for each test events: - - reason: after + - reason: beforeExecution tags: [ test ] - - testbook: # We need to update the TestBook (if configured) once each test is done + - videoDynamicInit: # initialize the video for each dynamic test + events: + - reason: beforeExecution + tags: [ dynamicTest ] + - video: # add the screenshot to the video + events: + - primaryId: screenshot + reason: screenshot + - videoDynamic: # add the screenshot to the dynamic video + events: + - primaryId: screenshot + reason: screenshot + - extentTest: # add an entry to the Extent Report once each test is done events: - reason: after tags: [ test ] - - video: # We need to finalize the video once each test is done + - videoFinalizer: # finalize the video once each test is done events: - reason: after - tags: [ test, dynamicTest ] - - videoDynamic: # We need to finalize the video once each dynamic test is done + tags: [ test, testFactory ] + - videoDynamicFinalizer: # finalize the video once each dynamic test is done events: - reason: after tags: [ dynamicTest ] - - driver: # We need to close the driver once each test is done + - testbook: # update the TestBook once each test is done + events: + - reason: after + tags: [ test ] + - driver: # close the driver once each test is done events: - reason: after tags: [ test, testFactory ] diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java index fabab6ed..bf7d6d5e 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java @@ -5,14 +5,17 @@ import com.aventstack.extentreports.Status; import com.aventstack.extentreports.model.Media; import io.github.giulong.spectrum.interfaces.Shared; +import io.github.giulong.spectrum.pojos.Screenshot; import io.github.giulong.spectrum.types.TestData; import io.github.giulong.spectrum.utils.*; +import io.github.giulong.spectrum.utils.events.EventsDispatcher; import io.github.giulong.spectrum.utils.video.Video; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -33,16 +36,18 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; import static com.aventstack.extentreports.Status.*; import static io.github.giulong.spectrum.SpectrumEntity.HASH_ALGORITHM; import static io.github.giulong.spectrum.enums.Frame.MANUAL; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.pojos.Screenshot.SCREENSHOT; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.*; -import static org.openqa.selenium.OutputType.BYTES; class SpectrumEntityTest { @@ -53,7 +58,6 @@ class SpectrumEntityTest { private final String msg = "msg"; private final String tag = "tag"; - private final String screenshotName = "screenshotName"; private final int frameNumber = 123; private final byte[] bytes = new byte[]{1, 2, 3}; private final byte[] digest = new byte[]{4, 5, 6}; @@ -97,9 +101,6 @@ class SpectrumEntityTest { @Mock private Path path; - @Mock - private Path reportsFolder; - @Mock private Path downloadsFolderPath; @@ -109,11 +110,23 @@ class SpectrumEntityTest { @Mock private File file; + @Mock + private EventsDispatcher eventsDispatcher; + @Mock private MediaEntityBuilder mediaEntityBuilder; @Mock - private Media screenshot; + private TestContext testContext; + + @Mock + private ExtensionContext context; + + @Mock + private Media media; + + @Mock + private Screenshot screenshot; @Mock private MessageDigest messageDigest; @@ -132,6 +145,7 @@ void beforeEach() { Reflections.setField("configuration", spectrumEntity, configuration); Reflections.setField("htmlUtils", spectrumEntity, htmlUtils); Reflections.setField("fileUtils", spectrumEntity, fileUtils); + Reflections.setField("eventsDispatcher", spectrumEntity, eventsDispatcher); pathMockedStatic = mockStatic(Path.class); filesMockedStatic = mockStatic(Files.class); @@ -149,19 +163,16 @@ void afterEach() { @SneakyThrows private void addScreenshotToReportStubs() { - when(fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest)).thenReturn(screenshotName); - when(testData.getScreenshotFolderPath()).thenReturn(path); - when(((TakesScreenshot) webDriver).getScreenshotAs(BYTES)).thenReturn(new byte[]{1, 2, 3}); + when(testContext.get(EXTENSION_CONTEXT, ExtensionContext.class)).thenReturn(context); + when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); + when(screenshot.getPath()).thenReturn(path); - when(fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest)).thenReturn(screenshotName); - when(testData.getScreenshotFolderPath()).thenReturn(reportsFolder); - when(reportsFolder.resolve(screenshotName)).thenReturn(path); when(statefulExtentTest.getCurrentNode()).thenReturn(extentTest); when(configuration.getVideo()).thenReturn(video); when(video.getAndIncrementFrameNumberFor(testData, MANUAL)).thenReturn(frameNumber); when(htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message")).thenReturn(tag); when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); - when(mediaEntityBuilder.build()).thenReturn(screenshot); + when(mediaEntityBuilder.build()).thenReturn(media); } @Test @@ -210,16 +221,13 @@ void hover() { @Test @DisplayName("screenshot should delegate to addScreenshotToReport") void screenshot() { - when(fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest)).thenReturn(screenshotName); - when(testData.getScreenshotFolderPath()).thenReturn(path); - when(((TakesScreenshot) webDriver).getScreenshotAs(BYTES)).thenReturn(new byte[]{1, 2, 3}); + when(testContext.get(EXTENSION_CONTEXT, ExtensionContext.class)).thenReturn(context); + when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); + when(screenshot.getPath()).thenReturn(path); - when(fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest)).thenReturn(screenshotName); - when(testData.getScreenshotFolderPath()).thenReturn(reportsFolder); - when(reportsFolder.resolve(screenshotName)).thenReturn(path); when(statefulExtentTest.getCurrentNode()).thenReturn(extentTest); when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); - when(mediaEntityBuilder.build()).thenReturn(screenshot); + when(mediaEntityBuilder.build()).thenReturn(media); assertEquals(spectrumEntity, spectrumEntity.screenshot()); @@ -273,23 +281,23 @@ void failWithScreenshot() { void addScreenshotToReport() { final Status status = INFO; - when(fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest)).thenReturn(screenshotName); - when(testData.getScreenshotFolderPath()).thenReturn(reportsFolder); - when(reportsFolder.resolve(screenshotName)).thenReturn(path); + when(testContext.get(EXTENSION_CONTEXT, ExtensionContext.class)).thenReturn(context); + when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); + when(screenshot.getPath()).thenReturn(path); when(statefulExtentTest.getCurrentNode()).thenReturn(extentTest); when(configuration.getVideo()).thenReturn(video); when(video.getAndIncrementFrameNumberFor(testData, MANUAL)).thenReturn(frameNumber); when(htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message")).thenReturn(tag); when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); - when(mediaEntityBuilder.build()).thenReturn(screenshot); - - when(((TakesScreenshot) webDriver).getScreenshotAs(BYTES)).thenReturn(new byte[]{1, 2, 3}); + when(mediaEntityBuilder.build()).thenReturn(media); final Media actual = spectrumEntity.addScreenshotToReport(msg, status); - assertEquals(screenshot, actual); + assertEquals(media, actual); + verify(eventsDispatcher).fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); verify(extentTest).log(status, tag, actual); + verifyNoMoreInteractions(eventsDispatcher); } @Test diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptorTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptorTest.java index f26fbd08..4e20feb3 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptorTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/interceptors/SpectrumInterceptorTest.java @@ -122,6 +122,7 @@ private void commonStubs() { @SuppressWarnings("checkstyle:IllegalThrows") private void commonVerifications() throws Throwable { + verify(testData).setDynamic(true); verify(testData).setFrameNumber(0); verify(testData).setDisplayName(displayName); verify(testData).setDynamicVideoPath(dynamicVideoPath); @@ -130,6 +131,7 @@ private void commonVerifications() throws Throwable { verify(statefulExtentTest).closeNode(); verify(eventsDispatcher).fire(className, displayName, BEFORE, null, Set.of(DYNAMIC_TEST), context); + verify(eventsDispatcher).fire(className, displayName, BEFORE_EXECUTION, null, Set.of(DYNAMIC_TEST), context); verify(invocation).proceed(); verify(contextManager).initWithParentFor(context); } @@ -148,6 +150,7 @@ void interceptDynamicTest(final boolean videoDisabled, final boolean attach) thr commonVerifications(); verify(eventsDispatcher).fire(className, displayName, AFTER, SUCCESSFUL, Set.of(DYNAMIC_TEST), context); + verifyNoMoreInteractions(eventsDispatcher); verifyNoInteractions(extentReporter); } @@ -176,6 +179,7 @@ void interceptDynamicTestAttachVideo() throws Throwable { commonVerifications(); verify(eventsDispatcher).fire(className, displayName, AFTER, SUCCESSFUL, Set.of(DYNAMIC_TEST), context); verify(extentReporter).attachVideo(extentTest, videoExtentTest, String.format("%s-%s", testId, displayName), dynamicVideoPath); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -190,6 +194,7 @@ void interceptDynamicTestThrow() throws Throwable { commonVerifications(); verify(eventsDispatcher).fire(className, displayName, AFTER, FAILED, Set.of(DYNAMIC_TEST), context); + verifyNoMoreInteractions(eventsDispatcher); verifyNoInteractions(extentReporter); } } diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolverTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolverTest.java index 4567df63..035357cc 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolverTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/DriverResolverTest.java @@ -244,10 +244,8 @@ private void stubs() { when(ScreenshotConsumer.builder()).thenReturn(screenshotConsumerBuilder); when(screenshotConsumerBuilder.enabled(true)).thenReturn(screenshotConsumerBuilder); - when(screenshotConsumerBuilder.driver((TakesScreenshot) webDriver)).thenReturn(screenshotConsumerBuilder); - when(screenshotConsumerBuilder.statefulExtentTest(statefulExtentTest)).thenReturn(screenshotConsumerBuilder); - when(screenshotConsumerBuilder.testData(testData)).thenReturn(screenshotConsumerBuilder); when(screenshotConsumerBuilder.video(video)).thenReturn(screenshotConsumerBuilder); + when(screenshotConsumerBuilder.context(context)).thenReturn(screenshotConsumerBuilder); when(screenshotConsumerBuilder.build()).thenReturn(screenshotConsumer); when(TestStepBuilderConsumer.builder()).thenReturn(testStepBuilderConsumerBuilder); diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolverTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolverTest.java index 45f8687e..8d3cd1e1 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolverTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestContextResolverTest.java @@ -11,11 +11,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; -import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.TEST_CONTEXT; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class TestContextResolverTest { @@ -45,12 +43,12 @@ void beforeEach() { @Test @DisplayName("resolveParameter should return an instance of TestContext") void resolveParameter() { - when(context.getStore(GLOBAL)).thenReturn(store); when(contextManager.initFor(context)).thenReturn(testContext); final TestContext actual = testContextResolver.resolveParameter(parameterContext, context); assertEquals(testContext, actual); - verify(store).put(TEST_CONTEXT, actual); + verify(testContext).put(EXTENSION_CONTEXT, context); + verifyNoInteractions(store); } } diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcherTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcherTest.java index fa2e5a0d..2e9079df 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcherTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/watchers/EventsWatcherTest.java @@ -5,6 +5,9 @@ import io.github.giulong.spectrum.utils.events.EventsDispatcher; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -15,6 +18,8 @@ import static io.github.giulong.spectrum.enums.Result.*; import static io.github.giulong.spectrum.utils.events.EventsDispatcher.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.*; class EventsWatcherTest { @@ -74,6 +79,7 @@ void testBeforeAll() { notifyClassStubs(); eventsWatcher.beforeAll(extensionContext); verify(eventsDispatcher).fire(className, null, BEFORE, null, Set.of(CLASS), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -82,6 +88,32 @@ void testBeforeEach() { notifyTestStubs(); eventsWatcher.beforeEach(extensionContext); verify(eventsDispatcher).fire(className, displayName, BEFORE, null, Set.of(TEST), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); + } + + @Test + @DisplayName("beforeTestExecution should dispatch only an event for regular tests") + void testBeforeTestExecution() throws NoSuchMethodException { + notifyTestStubs(); + + when(extensionContext.getRequiredTestMethod()).thenReturn(getClass().getDeclaredMethod("testAfterEach")); + + eventsWatcher.beforeTestExecution(extensionContext); + verify(eventsDispatcher).fire(className, displayName, BEFORE_EXECUTION, null, Set.of(TEST), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); + } + + @Test + @DisplayName("beforeTestExecution should dispatch two events for TestFactories") + void testBeforeTestExecutionTestFactory() throws NoSuchMethodException { + notifyTestStubs(); + + when(extensionContext.getRequiredTestMethod()).thenReturn(getClass().getDeclaredMethod("testFactoryMethod")); + + eventsWatcher.beforeTestExecution(extensionContext); + verify(eventsDispatcher).fire(className, displayName, BEFORE_EXECUTION, null, Set.of(TEST), extensionContext); + verify(eventsDispatcher).fire(className, displayName, BEFORE_EXECUTION, null, Set.of(TEST_FACTORY), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -93,6 +125,7 @@ void testAfterEachTestFactory() throws NoSuchMethodException { eventsWatcher.afterEach(extensionContext); verify(eventsDispatcher).fire(className, displayName, AFTER, SUCCESSFUL, Set.of(TEST_FACTORY), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -110,6 +143,7 @@ void testAfterAll() { notifyClassStubs(); eventsWatcher.afterAll(extensionContext); verify(eventsDispatcher).fire(className, null, AFTER, null, Set.of(CLASS), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -118,6 +152,7 @@ void testDisabled() { notifyTestStubs(); eventsWatcher.testDisabled(extensionContext, Optional.of("reason")); verify(eventsDispatcher).fire(className, displayName, AFTER, DISABLED, Set.of(TEST), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -126,6 +161,7 @@ void testSuccessful() { notifyTestStubs(); eventsWatcher.testSuccessful(extensionContext); verify(eventsDispatcher).fire(className, displayName, AFTER, SUCCESSFUL, Set.of(TEST), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -134,6 +170,7 @@ void testAborted() { notifyTestStubs(); eventsWatcher.testAborted(extensionContext, new RuntimeException()); verify(eventsDispatcher).fire(className, displayName, AFTER, ABORTED, Set.of(TEST), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -142,6 +179,23 @@ void testFailed() { notifyTestStubs(); eventsWatcher.testFailed(extensionContext, new RuntimeException()); verify(eventsDispatcher).fire(className, displayName, AFTER, FAILED, Set.of(TEST), extensionContext); + verifyNoMoreInteractions(eventsDispatcher); + } + + @DisplayName("isTestFactory should check if the method is annotated with @TestFactory") + @ParameterizedTest(name = "with method {0} we expect {1}") + @MethodSource("valuesProvider") + void isTestFactory(final String methodName, final boolean expected) throws NoSuchMethodException { + when(extensionContext.getRequiredTestMethod()).thenReturn(getClass().getDeclaredMethod(methodName)); + + assertEquals(expected, eventsWatcher.isTestFactory(extensionContext)); + } + + static Stream valuesProvider() { + return Stream.of( + arguments("testFactoryMethod", true), + arguments("testAfterEach", false) + ); } @Test @@ -154,6 +208,7 @@ void testNotifyClass() { notifyClassStubs(); eventsWatcher.notifyClass(extensionContext, reason, result, tags); verify(eventsDispatcher).fire(className, null, reason, result, tags, extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -166,5 +221,6 @@ void testNotifyTest() { notifyTestStubs(); eventsWatcher.notifyTest(extensionContext, reason, result, tags); verify(eventsDispatcher).fire(className, displayName, reason, result, tags, extensionContext); + verifyNoMoreInteractions(eventsDispatcher); } } diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java index 73dca806..c8d3a28f 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java @@ -5,7 +5,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; import java.io.File; import java.io.IOException; @@ -15,7 +17,6 @@ import java.nio.file.attribute.FileTime; import java.util.stream.Stream; -import static io.github.giulong.spectrum.enums.Frame.AUTO_AFTER; import static java.lang.System.lineSeparator; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.matchesPattern; @@ -26,7 +27,7 @@ class FileUtilsTest { private static final String DISPLAY_NAME = "displayName"; - private static final String UUID_REGEX = AUTO_AFTER.getValue() + "-" + DISPLAY_NAME + "-([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})\\.png"; + private static final String UUID_REGEX = DISPLAY_NAME + "-([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})\\.png"; @Mock private BasicFileAttributes basicFileAttributes; @@ -235,10 +236,10 @@ void getCreationTimeOf() throws IOException { } @Test - @DisplayName("getScreenshotNameFrom should return the name for the provided frame and display name") - void getScreenshotNameFrom() { + @DisplayName("buildScreenshotNameFrom should return the name for the provided frame and display name") + void buildScreenshotNameFrom() { when(statefulExtentTest.getDisplayName()).thenReturn(DISPLAY_NAME); - assertThat(fileUtils.getScreenshotNameFrom(AUTO_AFTER, statefulExtentTest), matchesPattern(UUID_REGEX)); + assertThat(fileUtils.buildScreenshotNameFrom(statefulExtentTest), matchesPattern(UUID_REGEX)); } } diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java index 4ad1e1e4..7a551d85 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java @@ -1,16 +1,15 @@ package io.github.giulong.spectrum.utils; +import io.github.giulong.spectrum.pojos.Screenshot; import io.github.giulong.spectrum.types.TestData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.*; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; import java.io.IOException; import java.nio.file.Files; @@ -18,24 +17,44 @@ import java.util.List; import java.util.Map; +import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.StatefulExtentTestResolver.STATEFUL_EXTENT_TEST; +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.matchesPattern; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.openqa.selenium.OutputType.BYTES; class HtmlUtilsTest { - private static MockedStatic pathMockedStatic; - private static MockedStatic filesMockedStatic; + private MockedStatic pathMockedStatic; + private MockedStatic filesMockedStatic; + private MockedStatic screenshotMockedStatic; private final String testId = "testId"; + @Mock + private Screenshot.ScreenshotBuilder screenshotBuilder; + + @Mock + private ExtensionContext context; + + @Mock + private ExtensionContext.Store store; + + @Mock + private StatefulExtentTest statefulExtentTest; + + @Mock(extraInterfaces = TakesScreenshot.class) + private WebDriver driver; + + @Mock + private Screenshot screenshot; + @Mock private Path path; @@ -51,6 +70,9 @@ class HtmlUtilsTest { @Mock private FileUtils fileUtils; + @Captor + private ArgumentCaptor byteArrayArgumentCaptor; + @Captor private ArgumentCaptor> freeMarkerVarsArgumentCaptor; @@ -61,6 +83,7 @@ class HtmlUtilsTest { void beforeEach() { pathMockedStatic = mockStatic(Path.class); filesMockedStatic = mockStatic(Files.class); + screenshotMockedStatic = mockStatic(Screenshot.class); Reflections.setField("freeMarkerWrapper", htmlUtils, freeMarkerWrapper); Reflections.setField("fileUtils", htmlUtils, fileUtils); @@ -70,6 +93,7 @@ void beforeEach() { void afterEach() { pathMockedStatic.close(); filesMockedStatic.close(); + screenshotMockedStatic.close(); } @Test @@ -137,6 +161,31 @@ void buildFrameTagFor() { assertEquals(expectedParams, freeMarkerVarsArgumentCaptor.getValue()); } + @Test + @DisplayName("buildScreenshotFrom should return the screenshot instance") + void buildScreenshotFrom() { + final String screenshotName = "screenshotName"; + final byte[] data = new byte[]{1, 2, 3}; + + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(STATEFUL_EXTENT_TEST, StatefulExtentTest.class)).thenReturn(statefulExtentTest); + when(fileUtils.buildScreenshotNameFrom(statefulExtentTest)).thenReturn(screenshotName); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + when(testData.getScreenshotFolderPath()).thenReturn(path); + when(path.resolve(screenshotName)).thenReturn(path2); + when(store.get(DRIVER, WebDriver.class)).thenReturn(driver); + when(((TakesScreenshot) driver).getScreenshotAs(BYTES)).thenReturn(data); + + when(Screenshot.builder()).thenReturn(screenshotBuilder); + when(screenshotBuilder.name(screenshotName)).thenReturn(screenshotBuilder); + when(screenshotBuilder.path(path2)).thenReturn(screenshotBuilder); + when(screenshotBuilder.data(byteArrayArgumentCaptor.capture())).thenReturn(screenshotBuilder); + when(screenshotBuilder.build()).thenReturn(screenshot); + + assertEquals(screenshot, htmlUtils.buildScreenshotFrom(context)); + assertArrayEquals(data, byteArrayArgumentCaptor.getValue()); + } + @Test @DisplayName("inline should call both the inlineImagesOf and inlineVideosOf on the provided html and return the one with all of those replaced") void inline() throws IOException { diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/EventsDispatcherTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/EventsDispatcherTest.java index 2aeb8033..3f7d1918 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/EventsDispatcherTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/EventsDispatcherTest.java @@ -15,6 +15,7 @@ import org.mockito.MockedStatic; import java.util.List; +import java.util.Map; import java.util.Set; import static io.github.giulong.spectrum.enums.Result.SUCCESSFUL; @@ -24,6 +25,8 @@ class EventsDispatcherTest { + private final String primaryId = "primaryId"; + private MockedStatic eventMockedStatic; @Mock @@ -32,6 +35,9 @@ class EventsDispatcherTest { @Mock private Summary summary; + @Mock + private Map payload; + @Mock private ExtensionContext extensionContext; @@ -80,6 +86,7 @@ void sessionOpened() { when(eventBuilder.result(null)).thenReturn(eventBuilder); when(eventBuilder.tags(tags)).thenReturn(eventBuilder); when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.sessionOpened(); @@ -101,6 +108,7 @@ void sessionClosed() { when(eventBuilder.result(result)).thenReturn(eventBuilder); when(eventBuilder.tags(tags)).thenReturn(eventBuilder); when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.sessionClosed(); @@ -121,6 +129,7 @@ void fire() { when(eventBuilder.result(null)).thenReturn(eventBuilder); when(eventBuilder.tags(tags)).thenReturn(eventBuilder); when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.fire(reason, tags); @@ -145,6 +154,7 @@ void fireReasonTagsResult() { when(eventBuilder.result(result)).thenReturn(eventBuilder); when(eventBuilder.tags(tags)).thenReturn(eventBuilder); when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.fire(reason, tags, result); @@ -153,11 +163,33 @@ void fireReasonTagsResult() { verify(consumer2).match(event); } + @Test + @DisplayName("fire should build an event with the provided primaryId, reason, and payload and call match on every consumer") + void firePrimaryIdReasonPayload() { + final String reason = AFTER; + + when(configuration.getEventsConsumers()).thenReturn(List.of(consumer1, consumer2)); + + when(Event.builder()).thenReturn(eventBuilder); + when(eventBuilder.primaryId(primaryId)).thenReturn(eventBuilder); + when(eventBuilder.secondaryId(null)).thenReturn(eventBuilder); + when(eventBuilder.reason(reason)).thenReturn(eventBuilder); + when(eventBuilder.result(null)).thenReturn(eventBuilder); + when(eventBuilder.tags(null)).thenReturn(eventBuilder); + when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(payload)).thenReturn(eventBuilder); + when(eventBuilder.build()).thenReturn(event); + + eventsDispatcher.fire(primaryId, reason, payload); + + verify(consumer1).match(event); + verify(consumer2).match(event); + } + @Test @DisplayName("fire should build an event with the provided primaryId and reason and call match on every consumer") void firePrimaryIdAndReason() { final String reason = AFTER; - final String primaryId = "primaryId"; when(configuration.getEventsConsumers()).thenReturn(List.of(consumer1, consumer2)); @@ -168,6 +200,7 @@ void firePrimaryIdAndReason() { when(eventBuilder.result(null)).thenReturn(eventBuilder); when(eventBuilder.tags(null)).thenReturn(eventBuilder); when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.fire(primaryId, reason); @@ -180,7 +213,6 @@ void firePrimaryIdAndReason() { @DisplayName("fire should build an event with the provided primaryId, secondaryId and reason and call match on every consumer") void firePrimaryIdAndSecondaryIdAndReason() { final String reason = AFTER; - final String primaryId = "primaryId"; final String secondaryId = "secondaryId"; when(configuration.getEventsConsumers()).thenReturn(List.of(consumer1, consumer2)); @@ -192,6 +224,7 @@ void firePrimaryIdAndSecondaryIdAndReason() { when(eventBuilder.result(null)).thenReturn(eventBuilder); when(eventBuilder.tags(null)).thenReturn(eventBuilder); when(eventBuilder.context(null)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.fire(primaryId, secondaryId, reason); @@ -218,6 +251,7 @@ void fireAllParams() { when(eventBuilder.result(result)).thenReturn(eventBuilder); when(eventBuilder.tags(tags)).thenReturn(eventBuilder); when(eventBuilder.context(extensionContext)).thenReturn(eventBuilder); + when(eventBuilder.payload(Map.of())).thenReturn(eventBuilder); when(eventBuilder.build()).thenReturn(event); eventsDispatcher.fire(className, testName, reason, result, tags, extensionContext); diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoConsumerTest.java deleted file mode 100644 index a965d3cc..00000000 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoConsumerTest.java +++ /dev/null @@ -1,518 +0,0 @@ -package io.github.giulong.spectrum.utils.events; - -import io.github.giulong.spectrum.enums.Result; -import io.github.giulong.spectrum.pojos.events.Event; -import io.github.giulong.spectrum.types.TestData; -import io.github.giulong.spectrum.utils.Configuration; -import io.github.giulong.spectrum.utils.ContextManager; -import io.github.giulong.spectrum.utils.Reflections; -import io.github.giulong.spectrum.utils.video.Video; -import org.jcodec.api.awt.AWTSequenceEncoder; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.*; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.WebDriver; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.stream.Stream; - -import static io.github.giulong.spectrum.SpectrumEntity.HASH_ALGORITHM; -import static io.github.giulong.spectrum.enums.Result.*; -import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.ORIGINAL_DRIVER; -import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; -import static java.awt.image.BufferedImage.TYPE_INT_RGB; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.*; - -class VideoConsumerTest { - - private static final String CLASS_NAME = "className"; - private static final String METHOD_NAME = "methodName"; - private static final int WIDTH = 11; - private static final int HEIGHT = 11; - private static final int EVEN_WIDTH = WIDTH + 1; - private static final int EVEN_HEIGHT = HEIGHT + 1; - - private static MockedStatic awtSequenceEncoderMockedStatic; - private static MockedStatic imageIOMockedStatic; - private static MockedStatic filesMockedStatic; - - @Mock - private ExtensionContext context; - - @Mock - private AWTSequenceEncoder encoder; - - @Mock - private File videoFile; - - @Mock - private BufferedImage bufferedImage; - - @Mock - private Graphics2D graphics2D; - - @Mock - private WebDriver driver; - - @Mock - private WebDriver.Options options; - - @Mock - private WebDriver.Window window; - - @Mock - private Dimension dimension; - - @Mock - private Video video; - - @Mock - private ContextManager contextManager; - - @Mock - private TestData testData; - - @Mock - private Path screenshotFolderPath; - - @Mock - private Path screenshotPath1; - - @Mock - private Path screenshotPath2; - - @Mock - private Path screenshotPath3; - - @Mock - private File screenshot1; - - @Mock - private File screenshot2; - - @Mock - private File screenshot3; - - @Mock - private Path videoPath; - - @Mock - private MessageDigest messageDigest; - - @Mock - private Event event; - - @Mock - private Configuration configuration; - - @Captor - private ArgumentCaptor urlArgumentCaptor; - - @Captor - private ArgumentCaptor byteArrayArgumentCaptor; - - @InjectMocks - private VideoConsumer videoConsumer; - - @BeforeEach - void beforeEach() { - Reflections.setField("configuration", videoConsumer, configuration); - Reflections.setField("contextManager", videoConsumer, contextManager); - - awtSequenceEncoderMockedStatic = mockStatic(AWTSequenceEncoder.class); - imageIOMockedStatic = mockStatic(ImageIO.class); - filesMockedStatic = mockStatic(Files.class); - } - - @AfterEach - void afterEach() { - awtSequenceEncoderMockedStatic.close(); - imageIOMockedStatic.close(); - filesMockedStatic.close(); - } - - @DisplayName("shouldAccept should check if the test and the video are disabled") - @ParameterizedTest(name = "with result {0} we expect {1}") - @MethodSource("shouldAcceptValuesProvider") - void shouldAccept(final Result result, final boolean videoDisabled, final boolean expected) { - when(event.getResult()).thenReturn(result); - lenient().when(configuration.getVideo()).thenReturn(video); - lenient().when(video.isDisabled()).thenReturn(videoDisabled); - - assertEquals(expected, videoConsumer.shouldAccept(event)); - } - - static Stream shouldAcceptValuesProvider() { - return Stream.of( - arguments(DISABLED, true, false), - arguments(SUCCESSFUL, true, false), - arguments(DISABLED, false, false), - arguments(SUCCESSFUL, false, true) - ); - } - - @Test - @DisplayName("accept should notify the VideoEncoder that the test is done") - void accept() throws IOException { - final int width = 1; - final int height = 3; - - when(configuration.getVideo()).thenReturn(video); - when(testData.getClassName()).thenReturn(CLASS_NAME); - when(testData.getMethodName()).thenReturn(METHOD_NAME); - when(testData.getScreenshotFolderPath()).thenReturn(screenshotFolderPath); - when(testData.getVideoPath()).thenReturn(videoPath); - when(videoPath.toFile()).thenReturn(videoFile); - when(Files.walk(screenshotFolderPath)).thenReturn(Stream.of(screenshotPath1, screenshotPath2, screenshotPath3)); - when(screenshotPath1.toFile()).thenReturn(screenshot1); - when(screenshotPath2.toFile()).thenReturn(screenshot2); - when(screenshotPath3.toFile()).thenReturn(screenshot3); - when(screenshot1.isFile()).thenReturn(true); - when(screenshot2.isFile()).thenReturn(false); - when(screenshot3.isFile()).thenReturn(true); - - when(event.getContext()).thenReturn(context); - when(contextManager.get(context, TEST_DATA, TestData.class)).thenReturn(testData); - - awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); - imageIOMockedStatic.when(() -> ImageIO.read(screenshot1)).thenReturn(bufferedImage); - imageIOMockedStatic.when(() -> ImageIO.read(screenshot3)).thenReturn(bufferedImage); - - when(video.isSkipDuplicateFrames()).thenReturn(true); - - // isNewFrame 1 - when(screenshot1.toPath()).thenReturn(screenshotPath1); - when(Files.readAllBytes(screenshotPath1)).thenReturn(new byte[]{1}); - // isNewFrame 3 - when(screenshot3.toPath()).thenReturn(screenshotPath3); - when(Files.readAllBytes(screenshotPath3)).thenReturn(new byte[]{3}); - - // chooseDimensionFor - when(video.getWidth()).thenReturn(width); - when(video.getHeight()).thenReturn(height); - - // resize - MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { - assertEquals(width + 1, executionContext.arguments().getFirst()); - assertEquals(height + 1, executionContext.arguments().get(1)); - assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); - - when(mock.createGraphics()).thenReturn(graphics2D); - }); - - videoConsumer.accept(event); - - final BufferedImage resizedImage1 = bufferedImageMockedConstruction.constructed().getFirst(); - final BufferedImage resizedImage2 = bufferedImageMockedConstruction.constructed().get(1); - verify(encoder).encodeImage(resizedImage1); - verify(encoder).encodeImage(resizedImage2); - verify(encoder).finish(); - - bufferedImageMockedConstruction.close(); - } - - @Test - @DisplayName("accept should notify the VideoEncoder that the test is done, with a skipped duplicate frame") - void acceptOneDuplicateFrame() throws IOException { - final int width = 1; - final int height = 3; - - when(configuration.getVideo()).thenReturn(video); - when(testData.getClassName()).thenReturn(CLASS_NAME); - when(testData.getMethodName()).thenReturn(METHOD_NAME); - when(testData.getScreenshotFolderPath()).thenReturn(screenshotFolderPath); - when(testData.getVideoPath()).thenReturn(videoPath); - when(videoPath.toFile()).thenReturn(videoFile); - when(Files.walk(screenshotFolderPath)).thenReturn(Stream.of(screenshotPath1, screenshotPath2, screenshotPath3)); - when(screenshotPath1.toFile()).thenReturn(screenshot1); - when(screenshotPath2.toFile()).thenReturn(screenshot2); - when(screenshotPath3.toFile()).thenReturn(screenshot3); - when(screenshot1.isFile()).thenReturn(true); - when(screenshot2.isFile()).thenReturn(false); - when(screenshot3.isFile()).thenReturn(true); - - when(event.getContext()).thenReturn(context); - when(contextManager.get(context, TEST_DATA, TestData.class)).thenReturn(testData); - when(contextManager.get(context, ORIGINAL_DRIVER, WebDriver.class)).thenReturn(driver); - - awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); - imageIOMockedStatic.when(() -> ImageIO.read(screenshot1)).thenReturn(bufferedImage); - imageIOMockedStatic.when(() -> ImageIO.read(screenshot3)).thenReturn(bufferedImage); - - when(video.isSkipDuplicateFrames()).thenReturn(true); - - // isNewFrame 1 - when(screenshot1.toPath()).thenReturn(screenshotPath1); - when(Files.readAllBytes(screenshotPath1)).thenReturn(new byte[]{1}); - // isNewFrame 3 - when(screenshot3.toPath()).thenReturn(screenshotPath3); - when(Files.readAllBytes(screenshotPath3)).thenReturn(new byte[]{1}); - - // chooseDimensionFor - when(video.getWidth()).thenReturn(width); - when(video.getHeight()).thenReturn(height); - - // resize - MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { - assertEquals(width + 1, executionContext.arguments().getFirst()); - assertEquals(height + 1, executionContext.arguments().get(1)); - assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); - - when(mock.createGraphics()).thenReturn(graphics2D); - }); - - videoConsumer.accept(event); - - final BufferedImage resizedImage1 = bufferedImageMockedConstruction.constructed().getFirst(); - verify(encoder).encodeImage(resizedImage1); - verify(encoder).finish(); - - bufferedImageMockedConstruction.close(); - } - - @Test - @DisplayName("accept should notify the VideoEncoder that the test is done without skipping duplicate frames") - void acceptNoSkipDuplicates() throws IOException { - final int width = 1; - final int height = 3; - - when(configuration.getVideo()).thenReturn(video); - when(testData.getClassName()).thenReturn(CLASS_NAME); - when(testData.getMethodName()).thenReturn(METHOD_NAME); - when(testData.getScreenshotFolderPath()).thenReturn(screenshotFolderPath); - when(testData.getVideoPath()).thenReturn(videoPath); - when(videoPath.toFile()).thenReturn(videoFile); - when(Files.walk(screenshotFolderPath)).thenReturn(Stream.of(screenshotPath1, screenshotPath2, screenshotPath3)); - when(screenshotPath1.toFile()).thenReturn(screenshot1); - when(screenshotPath2.toFile()).thenReturn(screenshot2); - when(screenshotPath3.toFile()).thenReturn(screenshot3); - when(screenshot1.isFile()).thenReturn(true); - when(screenshot2.isFile()).thenReturn(false); - when(screenshot3.isFile()).thenReturn(true); - - when(event.getContext()).thenReturn(context); - when(contextManager.get(context, TEST_DATA, TestData.class)).thenReturn(testData); - - awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); - imageIOMockedStatic.when(() -> ImageIO.read(screenshot1)).thenReturn(bufferedImage); - imageIOMockedStatic.when(() -> ImageIO.read(screenshot3)).thenReturn(bufferedImage); - - when(video.isSkipDuplicateFrames()).thenReturn(false); - - // chooseDimensionFor - when(video.getWidth()).thenReturn(width); - when(video.getHeight()).thenReturn(height); - - // resize - MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { - assertEquals(width + 1, executionContext.arguments().getFirst()); - assertEquals(height + 1, executionContext.arguments().get(1)); - assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); - - when(mock.createGraphics()).thenReturn(graphics2D); - }); - - videoConsumer.accept(event); - - final BufferedImage resizedImage1 = bufferedImageMockedConstruction.constructed().getFirst(); - final BufferedImage resizedImage2 = bufferedImageMockedConstruction.constructed().get(1); - verify(encoder).encodeImage(resizedImage1); - verify(encoder).encodeImage(resizedImage2); - verify(encoder).finish(); - - bufferedImageMockedConstruction.close(); - } - - @Test - @DisplayName("accept should add the no-video.png if no frames were added") - void acceptNoFramesAdded() throws IOException, URISyntaxException { - when(configuration.getVideo()).thenReturn(video); - when(testData.getClassName()).thenReturn(CLASS_NAME); - when(testData.getMethodName()).thenReturn(METHOD_NAME); - when(testData.getScreenshotFolderPath()).thenReturn(screenshotFolderPath); - when(testData.getVideoPath()).thenReturn(videoPath); - when(videoPath.toFile()).thenReturn(videoFile); - when(Files.walk(screenshotFolderPath)).thenReturn(Stream.of()); - - when(event.getContext()).thenReturn(context); - when(contextManager.get(context, TEST_DATA, TestData.class)).thenReturn(testData); - - awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); - imageIOMockedStatic.when(() -> ImageIO.read(urlArgumentCaptor.capture())).thenReturn(bufferedImage); - - videoConsumer.accept(event); - - assertEquals("no-video.png", Path.of(urlArgumentCaptor.getValue().toURI()).getFileName().toString()); - verify(encoder).encodeImage(bufferedImage); - verify(encoder).finish(); - } - - @Test - @DisplayName("init should init the needed fields") - void init() throws NoSuchAlgorithmException { - final byte[] lastFrameDigest = new byte[]{1, 2, 3}; - - Reflections.setField("lastFrameDigest", videoConsumer, lastFrameDigest); - - videoConsumer.init(); - - assertNull(Reflections.getFieldValue("lastFrameDigest", videoConsumer)); - assertEquals(MessageDigest.getInstance(HASH_ALGORITHM).getAlgorithm(), (Reflections.getFieldValue("messageDigest", videoConsumer, MessageDigest.class)).getAlgorithm()); - } - - @Test - @DisplayName("getVideoPathFrom should return the video path from the provided testData") - void getVideoPathFrom() { - when(testData.getVideoPath()).thenReturn(videoPath); - - assertEquals(videoPath, videoConsumer.getVideoPathFrom(testData)); - } - - @Test - @DisplayName("filter should always return true for any provided file") - void filter() { - assertTrue(videoConsumer.filter(screenshot1, testData)); - } - - @Test - @DisplayName("chooseDimensionFor should return a new Dimension based on the provided video dimension") - void chooseDimensionFor() { - when(video.getWidth()).thenReturn(1); - when(video.getHeight()).thenReturn(3); - - final Dimension actual = videoConsumer.chooseDimensionFor(driver, video); - assertEquals(2, actual.getWidth()); - assertEquals(4, actual.getHeight()); - } - - @DisplayName("chooseDimensionFor should return a new Dimension based on webDriver's dimension, if at least one is lte 0") - @ParameterizedTest(name = "with width {0} and height {1}") - @MethodSource("dimensionProvider") - void chooseDimensionForProvided(final int width, final int height) { - final int menuBarsHeight = 123; - - reset(video); - when(video.getWidth()).thenReturn(width); - - if (width >= 1) { // short-circuit - when(video.getHeight()).thenReturn(height); - } - when(driver.manage()).thenReturn(options); - when(options.window()).thenReturn(window); - when(window.getSize()).thenReturn(dimension); - when(dimension.getWidth()).thenReturn(WIDTH); - when(dimension.getHeight()).thenReturn(HEIGHT); - when(video.getMenuBarsHeight()).thenReturn(menuBarsHeight); - - final Dimension actual = videoConsumer.chooseDimensionFor(driver, video); - - assertEquals(EVEN_WIDTH, actual.getWidth()); - assertEquals(EVEN_HEIGHT - menuBarsHeight - 1, actual.getHeight()); - } - - static Stream dimensionProvider() { - return Stream.of( - arguments(0, 0), - arguments(1, 0), - arguments(0, 1) - ); - } - - @DisplayName("makeItEven should increment the provided int if it's odd") - @ParameterizedTest(name = "with i {0} we expect {1}") - @MethodSource("valuesProvider") - void makeItEven(final int i, final int expected) { - assertEquals(expected, videoConsumer.makeItEven(i)); - } - - static Stream valuesProvider() { - return Stream.of( - arguments(0, 0), - arguments(1, 2) - ); - } - - @Test - @DisplayName("isNewFrame should return true if the provided screenshot is new") - void isNewFrame() throws IOException { - final byte[] lastFrameDigest = new byte[]{1, 2, 3}; - final byte[] screenshotBytes = new byte[]{4, 5, 6}; - final byte[] newFrameDigest = new byte[]{7, 8, 9}; - - Reflections.setField("lastFrameDigest", videoConsumer, lastFrameDigest); - - when(screenshot1.toPath()).thenReturn(screenshotPath1); - when(Files.readAllBytes(screenshotPath1)).thenReturn(screenshotBytes); - when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(newFrameDigest); - - assertTrue(videoConsumer.isNewFrame(screenshot1, testData)); - - assertArrayEquals(screenshotBytes, byteArrayArgumentCaptor.getValue()); - assertArrayEquals(newFrameDigest, Reflections.getFieldValue("lastFrameDigest", videoConsumer, byte[].class)); - } - - @Test - @DisplayName("isNewFrame should return false if the provided screenshot is not new") - void isNewFrameFalse() throws IOException { - final byte[] lastFrameDigest = new byte[]{1, 2, 3}; - final byte[] screenshotBytes = new byte[]{4, 5, 6}; - - Reflections.setField("lastFrameDigest", videoConsumer, lastFrameDigest); - - when(screenshot1.toPath()).thenReturn(screenshotPath1); - when(Files.readAllBytes(screenshotPath1)).thenReturn(screenshotBytes); - when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(lastFrameDigest); - - assertFalse(videoConsumer.isNewFrame(screenshot1, testData)); - - assertArrayEquals(screenshotBytes, byteArrayArgumentCaptor.getValue()); - assertArrayEquals(lastFrameDigest, Reflections.getFieldValue("lastFrameDigest", videoConsumer, byte[].class)); - } - - @Test - @DisplayName("resize should resize the image and return it") - void resize() { - final int width = 6; - final int height = 100; - - MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { - assertEquals(EVEN_WIDTH, executionContext.arguments().getFirst()); - assertEquals(EVEN_HEIGHT, executionContext.arguments().get(1)); - assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); - - when(mock.createGraphics()).thenReturn(graphics2D); - }); - - when(dimension.getWidth()).thenReturn(EVEN_HEIGHT); - when(dimension.getHeight()).thenReturn(EVEN_HEIGHT); - when(bufferedImage.getWidth()).thenReturn(width); - when(bufferedImage.getHeight()).thenReturn(height); - - final BufferedImage actual = videoConsumer.resize(bufferedImage, dimension); - - assertEquals(bufferedImageMockedConstruction.constructed().getFirst(), actual); - verify(graphics2D).drawImage(bufferedImage, 0, 0, width, EVEN_HEIGHT, null); - verify(graphics2D).dispose(); - - bufferedImageMockedConstruction.close(); - } -} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumerTest.java deleted file mode 100644 index 2e127556..00000000 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/VideoDynamicConsumerTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.github.giulong.spectrum.utils.events; - -import io.github.giulong.spectrum.types.TestData; -import io.github.giulong.spectrum.utils.Reflections; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.*; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.stream.Stream; - -import static io.github.giulong.spectrum.SpectrumEntity.HASH_ALGORITHM; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.*; - -class VideoDynamicConsumerTest { - - private static MockedStatic filesMockedStatic; - - @Mock - private TestData testData; - - @Mock - private Path dynamicVideoPath; - - @Mock - private File screenshot; - - @Mock - private Path screenshotPath; - - @Mock - private MessageDigest messageDigest; - - @Captor - private ArgumentCaptor byteArrayArgumentCaptor; - - @InjectMocks - private VideoDynamicConsumer videoDynamicConsumer; - - @BeforeEach - void beforeEach() { - filesMockedStatic = mockStatic(Files.class); - } - - @AfterEach - void afterEach() { - filesMockedStatic.close(); - } - - @Test - @DisplayName("init should init the needed fields") - void init() throws NoSuchAlgorithmException { - final byte[] lastFrameDigest = new byte[]{1, 2, 3}; - final String displayName = "displayName"; - - Reflections.setField("lastFrameDigest", videoDynamicConsumer, lastFrameDigest); - Reflections.setField("lastFrameDisplayName", videoDynamicConsumer, displayName); - - videoDynamicConsumer.init(); - - assertNull(Reflections.getFieldValue("lastFrameDigest", videoDynamicConsumer)); - assertNull(Reflections.getFieldValue("lastFrameDisplayName", videoDynamicConsumer)); - assertEquals( - MessageDigest.getInstance(HASH_ALGORITHM).getAlgorithm(), - (Reflections.getFieldValue("messageDigest", videoDynamicConsumer, MessageDigest.class)).getAlgorithm()); - } - - @Test - @DisplayName("getVideoPathFrom should return the dynamic video path from the provided testData") - void getVideoPathFrom() { - when(testData.getDynamicVideoPath()).thenReturn(dynamicVideoPath); - - assertEquals(dynamicVideoPath, videoDynamicConsumer.getVideoPathFrom(testData)); - } - - @DisplayName("filter should return true if the provided file contains the displayName of the provided testData") - @ParameterizedTest(name = "with file name {0} we expect {1}") - @MethodSource("valuesProvider") - void filter(final String fileName, final boolean expected) { - final String displayName = "displayName"; - - when(screenshot.getName()).thenReturn(fileName); - lenient().when(testData.getDisplayName()).thenReturn(displayName); - - assertEquals(expected, videoDynamicConsumer.filter(screenshot, testData)); - } - - static Stream valuesProvider() { - return Stream.of( - arguments("abc-displayName-def12345-1234-1234-1234-123412345678.png", true), - arguments("abc-notMatchingDisplayName-def12345-1234-1234-1234-123412345678.png", false), - arguments("notMatchingAtAll", false) - ); - } - - @Test - @DisplayName("isNewFrame should return true if the display name of the provided testData is new") - void isNewFrame() { - final String displayName = "displayName"; - - when(testData.getDisplayName()).thenReturn(displayName); - - assertTrue(videoDynamicConsumer.isNewFrame(screenshot, testData)); - } - - @Test - @DisplayName("isNewFrame should delegate to the parent's implementation when the displayName of the provided testData is not new") - void isNewFrameOld() throws IOException { - final String displayName = "displayName"; - - when(testData.getDisplayName()).thenReturn(displayName); - - final byte[] lastFrameDigest = new byte[]{1, 2, 3}; - final byte[] screenshotBytes = new byte[]{4, 5, 6}; - final byte[] newFrameDigest = new byte[]{7, 8, 9}; - - Reflections.setField("lastFrameDigest", videoDynamicConsumer, lastFrameDigest); - Reflections.setField("lastFrameDisplayName", videoDynamicConsumer, displayName); - - when(screenshot.toPath()).thenReturn(screenshotPath); - when(Files.readAllBytes(screenshotPath)).thenReturn(screenshotBytes); - when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(newFrameDigest); - - assertTrue(videoDynamicConsumer.isNewFrame(screenshot, testData)); - - assertArrayEquals(screenshotBytes, byteArrayArgumentCaptor.getValue()); - assertArrayEquals(newFrameDigest, Reflections.getFieldValue("lastFrameDigest", videoDynamicConsumer, byte[].class)); - } -} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumerTest.java new file mode 100644 index 00000000..af7b5c2d --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoBaseConsumerTest.java @@ -0,0 +1,87 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.enums.Result; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import io.github.giulong.spectrum.utils.Configuration; +import io.github.giulong.spectrum.utils.Reflections; +import io.github.giulong.spectrum.utils.video.Video; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import static io.github.giulong.spectrum.enums.Result.DISABLED; +import static io.github.giulong.spectrum.enums.Result.SUCCESSFUL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +class VideoBaseConsumerTest { + + @Mock + private Configuration configuration; + + @Mock + private Video video; + + @Mock + private Event event; + + @Mock + private TestData testData; + + @Mock + private Path videoPath; + + @InjectMocks + private DummyVideoBaseConsumer videoBaseConsumer; + + @BeforeEach + void beforeEach() { + Reflections.setField("configuration", videoBaseConsumer, configuration); + } + + @DisplayName("shouldAccept should check if the test and the video are disabled") + @ParameterizedTest(name = "with result {0} we expect {1}") + @MethodSource("shouldAcceptValuesProvider") + void shouldAccept(final Result result, final boolean videoDisabled, final boolean expected) { + when(event.getResult()).thenReturn(result); + lenient().when(configuration.getVideo()).thenReturn(video); + lenient().when(video.isDisabled()).thenReturn(videoDisabled); + + assertEquals(expected, videoBaseConsumer.shouldAccept(event)); + } + + static Stream shouldAcceptValuesProvider() { + return Stream.of( + arguments(DISABLED, true, false), + arguments(SUCCESSFUL, true, false), + arguments(DISABLED, false, false), + arguments(SUCCESSFUL, false, true) + ); + } + + @Test + @DisplayName("getVideoPathFrom should return the video path from the provided testData") + void getVideoPathFrom() { + when(testData.getVideoPath()).thenReturn(videoPath); + + assertEquals(videoPath, videoBaseConsumer.getVideoPathFrom(testData)); + } + + private static final class DummyVideoBaseConsumer extends VideoBaseConsumer { + + @Override + public void accept(Event event) { + } + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoConsumerTest.java new file mode 100644 index 00000000..a3ac21c4 --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoConsumerTest.java @@ -0,0 +1,389 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import io.github.giulong.spectrum.utils.Configuration; +import io.github.giulong.spectrum.utils.Reflections; +import io.github.giulong.spectrum.utils.video.Video; +import org.jcodec.api.awt.AWTSequenceEncoder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.*; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.WebDriver; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Map; +import java.util.stream.Stream; + +import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.ORIGINAL_DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static io.github.giulong.spectrum.pojos.Screenshot.SCREENSHOT; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.*; + +class VideoConsumerTest { + + private static final int WIDTH = 11; + private static final int HEIGHT = 11; + private static final int EVEN_WIDTH = WIDTH + 1; + private static final int EVEN_HEIGHT = HEIGHT + 1; + + private static MockedStatic awtSequenceEncoderMockedStatic; + private static MockedStatic imageIOMockedStatic; + private static MockedStatic byteArrayInputStreamMockedStatic; + + @Mock + private ExtensionContext context; + + @Mock + private ExtensionContext.Store store; + + @Mock + private AWTSequenceEncoder encoder; + + @Mock + private File videoFile; + + @Mock + private BufferedImage bufferedImage; + + @Mock + private Graphics2D graphics2D; + + @Mock + private WebDriver driver; + + @Mock + private WebDriver.Options options; + + @Mock + private WebDriver.Window window; + + @Mock + private Dimension dimension; + + @Mock + private Video video; + + @Mock + private TestData testData; + + @Mock + private Screenshot screenshot; + + @Mock + private Path videoPath; + + @Mock + private MessageDigest messageDigest; + + @Mock + private Event event; + + @Mock + private Configuration configuration; + + @Mock + private Map payload; + + @Captor + private ArgumentCaptor byteArrayArgumentCaptor; + + @InjectMocks + private VideoConsumer videoConsumer; + + @BeforeEach + void beforeEach() { + Reflections.setField("configuration", videoConsumer, configuration); + Reflections.setField("messageDigest", videoConsumer, messageDigest); + + awtSequenceEncoderMockedStatic = mockStatic(AWTSequenceEncoder.class); + imageIOMockedStatic = mockStatic(ImageIO.class); + byteArrayInputStreamMockedStatic = mockStatic(ByteArrayInputStream.class); + } + + @AfterEach + void afterEach() { + awtSequenceEncoderMockedStatic.close(); + imageIOMockedStatic.close(); + byteArrayInputStreamMockedStatic.close(); + } + + @Test + @DisplayName("accept should encode the image is new") + void accept() throws IOException { + final int width = 1; + final int height = 3; + + acceptStubs(); + + when(testData.getVideoPath()).thenReturn(videoPath); + when(testData.getEncoders()).thenReturn(Map.of(videoPath, encoder)); + + awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); + + when(video.isSkipDuplicateFrames()).thenReturn(true); + when(store.get(ORIGINAL_DRIVER, WebDriver.class)).thenReturn(driver); + + // isNewFrame + final byte[] lastFrameDigest = new byte[]{1, 2, 3}; + final byte[] screenshotBytes = new byte[]{4, 5, 6}; + final byte[] newFrameDigest = new byte[]{7, 8, 9}; + when(testData.getLastFrameDigest()).thenReturn(lastFrameDigest); + when(screenshot.getData()).thenReturn(screenshotBytes); + when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(newFrameDigest); + + // chooseDimensionFor + when(video.getWidth()).thenReturn(width); + when(video.getHeight()).thenReturn(height); + + final MockedConstruction byteArrayInputStreamMockedConstruction = mockConstruction(ByteArrayInputStream.class, (mock, executionContext) -> { + assertEquals(screenshotBytes, executionContext.arguments().getFirst()); + + imageIOMockedStatic.when(() -> ImageIO.read(mock)).thenReturn(bufferedImage); + }); + + // resize + final MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { + assertEquals(width + 1, executionContext.arguments().getFirst()); + assertEquals(height + 1, executionContext.arguments().get(1)); + assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); + + when(mock.createGraphics()).thenReturn(graphics2D); + }); + + videoConsumer.accept(event); + + final BufferedImage resizedImage = bufferedImageMockedConstruction.constructed().getFirst(); + verify(encoder).encodeImage(resizedImage); + verifyNoMoreInteractions(encoder); + + bufferedImageMockedConstruction.close(); + byteArrayInputStreamMockedConstruction.close(); + } + + @Test + @DisplayName("accept should not encode the image if it's a duplicate frame") + void acceptDuplicateFrame() { + acceptStubs(); + + awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); + + when(video.isSkipDuplicateFrames()).thenReturn(true); + + // isNewFrame + final byte[] lastFrameDigest = new byte[]{1, 2, 3}; + final byte[] screenshotBytes = new byte[]{4, 5, 6}; + when(testData.getLastFrameDigest()).thenReturn(lastFrameDigest); + when(screenshot.getData()).thenReturn(screenshotBytes); + when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(lastFrameDigest); + + videoConsumer.accept(event); + + verifyNoInteractions(encoder); + } + + @Test + @DisplayName("accept should encode the image if duplicate frames should be skipped but the image is new") + void acceptDuplicateFrameNewFrame() throws IOException { + final int width = 1; + final int height = 3; + + acceptStubs(); + + when(testData.getVideoPath()).thenReturn(videoPath); + when(testData.getEncoders()).thenReturn(Map.of(videoPath, encoder)); + + awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(videoFile, 1)).thenReturn(encoder); + + when(video.isSkipDuplicateFrames()).thenReturn(false); + + final byte[] screenshotBytes = new byte[]{4, 5, 6}; + when(screenshot.getData()).thenReturn(screenshotBytes); + + // chooseDimensionFor + when(video.getWidth()).thenReturn(width); + when(video.getHeight()).thenReturn(height); + + final MockedConstruction byteArrayInputStreamMockedConstruction = mockConstruction(ByteArrayInputStream.class, (mock, executionContext) -> { + assertEquals(screenshotBytes, executionContext.arguments().getFirst()); + + imageIOMockedStatic.when(() -> ImageIO.read(mock)).thenReturn(bufferedImage); + }); + + // resize + final MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { + assertEquals(width + 1, executionContext.arguments().getFirst()); + assertEquals(height + 1, executionContext.arguments().get(1)); + assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); + + when(mock.createGraphics()).thenReturn(graphics2D); + }); + + videoConsumer.accept(event); + + final BufferedImage resizedImage = bufferedImageMockedConstruction.constructed().getFirst(); + verify(encoder).encodeImage(resizedImage); + verifyNoMoreInteractions(encoder); + + bufferedImageMockedConstruction.close(); + byteArrayInputStreamMockedConstruction.close(); + } + + @Test + @DisplayName("getVideoPathFrom should return the video path from the provided testData") + void getVideoPathFrom() { + when(testData.getVideoPath()).thenReturn(videoPath); + + assertEquals(videoPath, videoConsumer.getVideoPathFrom(testData)); + } + + @Test + @DisplayName("chooseDimensionFor should return a new Dimension based on the provided video dimension") + void chooseDimensionFor() { + when(video.getWidth()).thenReturn(1); + when(video.getHeight()).thenReturn(3); + + final Dimension actual = videoConsumer.chooseDimensionFor(driver, video); + assertEquals(2, actual.getWidth()); + assertEquals(4, actual.getHeight()); + } + + @DisplayName("chooseDimensionFor should return a new Dimension based on webDriver's dimension, if at least one is lte 0") + @ParameterizedTest(name = "with width {0} and height {1}") + @MethodSource("dimensionProvider") + void chooseDimensionForProvided(final int width, final int height) { + final int menuBarsHeight = 123; + + reset(video); + when(video.getWidth()).thenReturn(width); + + if (width >= 1) { // short-circuit + when(video.getHeight()).thenReturn(height); + } + when(driver.manage()).thenReturn(options); + when(options.window()).thenReturn(window); + when(window.getSize()).thenReturn(dimension); + when(dimension.getWidth()).thenReturn(WIDTH); + when(dimension.getHeight()).thenReturn(HEIGHT); + when(video.getMenuBarsHeight()).thenReturn(menuBarsHeight); + + final Dimension actual = videoConsumer.chooseDimensionFor(driver, video); + + assertEquals(EVEN_WIDTH, actual.getWidth()); + assertEquals(EVEN_HEIGHT - menuBarsHeight - 1, actual.getHeight()); + } + + static Stream dimensionProvider() { + return Stream.of( + arguments(0, 0), + arguments(1, 0), + arguments(0, 1) + ); + } + + @DisplayName("makeItEven should increment the provided int if it's odd") + @ParameterizedTest(name = "with i {0} we expect {1}") + @MethodSource("valuesProvider") + void makeItEven(final int i, final int expected) { + assertEquals(expected, videoConsumer.makeItEven(i)); + } + + static Stream valuesProvider() { + return Stream.of( + arguments(0, 0), + arguments(1, 2) + ); + } + + @Test + @DisplayName("isNewFrame should return true if the provided screenshot is new") + void isNewFrame() { + final byte[] lastFrameDigest = new byte[]{1, 2, 3}; + final byte[] screenshotBytes = new byte[]{4, 5, 6}; + final byte[] newFrameDigest = new byte[]{7, 8, 9}; + + when(testData.getLastFrameDigest()).thenReturn(lastFrameDigest); + + when(screenshot.getData()).thenReturn(screenshotBytes); + when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(newFrameDigest); + + assertTrue(videoConsumer.isNewFrame(screenshot, testData)); + + assertArrayEquals(screenshotBytes, byteArrayArgumentCaptor.getValue()); + verify(testData).setLastFrameDigest(newFrameDigest); + } + + @Test + @DisplayName("isNewFrame should return false if the provided screenshot is not new") + void isNewFrameFalse() { + final byte[] lastFrameDigest = new byte[]{1, 2, 3}; + final byte[] screenshotBytes = new byte[]{4, 5, 6}; + + when(testData.getLastFrameDigest()).thenReturn(lastFrameDigest); + + when(screenshot.getData()).thenReturn(screenshotBytes); + when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(lastFrameDigest); + + assertFalse(videoConsumer.isNewFrame(screenshot, testData)); + + assertArrayEquals(screenshotBytes, byteArrayArgumentCaptor.getValue()); + verify(testData, never()).setLastFrameDigest(any()); + } + + @Test + @DisplayName("resize should resize the image and return it") + void resize() { + final int width = 6; + final int height = 100; + + MockedConstruction bufferedImageMockedConstruction = mockConstruction(BufferedImage.class, (mock, executionContext) -> { + assertEquals(EVEN_WIDTH, executionContext.arguments().getFirst()); + assertEquals(EVEN_HEIGHT, executionContext.arguments().get(1)); + assertEquals(TYPE_INT_RGB, executionContext.arguments().get(2)); + + when(mock.createGraphics()).thenReturn(graphics2D); + }); + + when(dimension.getWidth()).thenReturn(EVEN_HEIGHT); + when(dimension.getHeight()).thenReturn(EVEN_HEIGHT); + when(bufferedImage.getWidth()).thenReturn(width); + when(bufferedImage.getHeight()).thenReturn(height); + + final BufferedImage actual = videoConsumer.resize(bufferedImage, dimension); + + assertEquals(bufferedImageMockedConstruction.constructed().getFirst(), actual); + verify(graphics2D).drawImage(bufferedImage, 0, 0, width, EVEN_HEIGHT, null); + verify(graphics2D).dispose(); + + bufferedImageMockedConstruction.close(); + } + + private void acceptStubs() { + when(configuration.getVideo()).thenReturn(video); + when(event.getPayload()).thenReturn(payload); + when(payload.get(EXTENSION_CONTEXT)).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + when(payload.get(SCREENSHOT)).thenReturn(screenshot); + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumerTest.java new file mode 100644 index 00000000..37197cd5 --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumerTest.java @@ -0,0 +1,191 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import io.github.giulong.spectrum.utils.Configuration; +import io.github.giulong.spectrum.utils.Reflections; +import io.github.giulong.spectrum.utils.video.Video; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Map; + +import static io.github.giulong.spectrum.enums.Result.DISABLED; +import static io.github.giulong.spectrum.enums.Result.SUCCESSFUL; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class VideoDynamicConsumerTest { + + @Mock + private Configuration configuration; + + @Mock + private Video video; + + @Mock + private TestData testData; + + @Mock + private ExtensionContext context; + + @Mock + private ExtensionContext.Store store; + + @Mock + private Path dynamicVideoPath; + + @Mock + private Screenshot screenshot; + + @Mock + private MessageDigest messageDigest; + + @Mock + private Event event; + + @Mock + private Map payload; + + @Captor + private ArgumentCaptor byteArrayArgumentCaptor; + + @InjectMocks + private VideoDynamicConsumer videoDynamicConsumer; + + @BeforeEach + void beforeEach() { + Reflections.setField("messageDigest", videoDynamicConsumer, messageDigest); + Reflections.setField("configuration", videoDynamicConsumer, configuration); + } + + @Test + @DisplayName("shouldAccept should return true only if the test is dynamic") + void shouldAccept() { + // super.shouldAccept + when(event.getResult()).thenReturn(SUCCESSFUL); + when(configuration.getVideo()).thenReturn(video); + when(video.isDisabled()).thenReturn(false); + + when(event.getPayload()).thenReturn(payload); + when(payload.get(EXTENSION_CONTEXT)).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + + when(testData.isDynamic()).thenReturn(true); + + assertTrue(videoDynamicConsumer.shouldAccept(event)); + } + + @Test + @DisplayName("shouldAccept should return false when the super condition doesn't match") + void shouldAcceptFalseSuperCondition() { + // super.shouldAccept + when(event.getResult()).thenReturn(DISABLED); + + assertFalse(videoDynamicConsumer.shouldAccept(event)); + } + + @Test + @DisplayName("shouldAccept should return false when the context is null") + void shouldAcceptFalseContextNull() { + // super.shouldAccept + when(event.getResult()).thenReturn(SUCCESSFUL); + when(configuration.getVideo()).thenReturn(video); + when(video.isDisabled()).thenReturn(false); + + when(event.getPayload()).thenReturn(payload); + when(payload.get(EXTENSION_CONTEXT)).thenReturn(null); + + assertFalse(videoDynamicConsumer.shouldAccept(event)); + } + + @Test + @DisplayName("shouldAccept should return false when test data is null") + void shouldAcceptFalseTestDataNull() { + // super.shouldAccept + when(event.getResult()).thenReturn(SUCCESSFUL); + when(configuration.getVideo()).thenReturn(video); + when(video.isDisabled()).thenReturn(false); + + when(event.getPayload()).thenReturn(payload); + when(payload.get(EXTENSION_CONTEXT)).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(null); + + assertFalse(videoDynamicConsumer.shouldAccept(event)); + } + + @Test + @DisplayName("shouldAccept should return false when test data is not null but dynamic is false") + void shouldAcceptFalseTestDataNotDynamic() { + // super.shouldAccept + when(event.getResult()).thenReturn(SUCCESSFUL); + when(configuration.getVideo()).thenReturn(video); + when(video.isDisabled()).thenReturn(false); + + when(event.getPayload()).thenReturn(payload); + when(payload.get(EXTENSION_CONTEXT)).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + when(testData.isDynamic()).thenReturn(false); + + assertFalse(videoDynamicConsumer.shouldAccept(event)); + } + + @Test + @DisplayName("getVideoPathFrom should return the dynamic video path from the provided testData") + void getVideoPathFrom() { + when(testData.getDynamicVideoPath()).thenReturn(dynamicVideoPath); + + assertEquals(dynamicVideoPath, videoDynamicConsumer.getVideoPathFrom(testData)); + } + + @Test + @DisplayName("isNewFrame should return true if the display name of the provided testData is new") + void isNewFrame() { + final String displayName = "displayName"; + + when(testData.getDisplayName()).thenReturn(displayName); + + assertTrue(videoDynamicConsumer.isNewFrame(screenshot, testData)); + } + + @Test + @DisplayName("isNewFrame should delegate to the parent's implementation when the displayName of the provided testData is not new") + void isNewFrameOld() { + final String displayName = "displayName"; + + when(testData.getDisplayName()).thenReturn(displayName); + when(testData.getLastFrameDisplayName()).thenReturn(displayName); + + // super.isNewFrame + final byte[] lastFrameDigest = new byte[]{1, 2, 3}; + final byte[] screenshotBytes = new byte[]{4, 5, 6}; + final byte[] newFrameDigest = new byte[]{7, 8, 9}; + when(testData.getLastFrameDigest()).thenReturn(lastFrameDigest); + when(screenshot.getData()).thenReturn(screenshotBytes); + when(messageDigest.digest(byteArrayArgumentCaptor.capture())).thenReturn(newFrameDigest); + + assertTrue(videoDynamicConsumer.isNewFrame(screenshot, testData)); + + verify(testData, never()).setLastFrameDisplayName(anyString()); + + // super.isNewFrame + assertArrayEquals(screenshotBytes, byteArrayArgumentCaptor.getValue()); + verify(testData).setLastFrameDigest(newFrameDigest); + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizerTest.java new file mode 100644 index 00000000..13d5cdc8 --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicFinalizerTest.java @@ -0,0 +1,32 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.types.TestData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class VideoDynamicFinalizerTest { + + @Mock + private TestData testData; + + @Mock + private Path path; + + @InjectMocks + private VideoDynamicFinalizer videoDynamicFinalizer; + + @Test + @DisplayName("getVideoPathFrom should return the dynamic video path from test data") + void getVideoPathFrom() { + when(testData.getDynamicVideoPath()).thenReturn(path); + + assertEquals(path, videoDynamicFinalizer.getVideoPathFrom(testData)); + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumerTest.java new file mode 100644 index 00000000..9a91a929 --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicInitConsumerTest.java @@ -0,0 +1,32 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.types.TestData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class VideoDynamicInitConsumerTest { + + @Mock + private TestData testData; + + @Mock + private Path path; + + @InjectMocks + private VideoDynamicInitConsumer videoDynamicInitConsumer; + + @Test + @DisplayName("getVideoPathFrom should return the dynamic video path from test data") + void getVideoPathFrom() { + when(testData.getDynamicVideoPath()).thenReturn(path); + + assertEquals(path, videoDynamicInitConsumer.getVideoPathFrom(testData)); + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizerTest.java new file mode 100644 index 00000000..295c8139 --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoFinalizerTest.java @@ -0,0 +1,108 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import org.jcodec.api.awt.AWTSequenceEncoder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.*; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.util.Map; + +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; +import static org.mockito.Mockito.*; + +class VideoFinalizerTest { + + private MockedStatic imageIOMockedStatic; + + @Mock + private Event event; + + @Mock + private ExtensionContext context; + + @Mock + private ExtensionContext.Store store; + + @Mock + private TestData testData; + + @Mock + private BufferedImage bufferedImage; + + @Mock + private Path path; + + @Mock + private Map encoders; + + @Mock + private AWTSequenceEncoder encoder; + + @Captor + private ArgumentCaptor urlArgumentCaptor; + + @InjectMocks + private VideoFinalizer videoFinalizer; + + @BeforeEach + void beforeEach() { + imageIOMockedStatic = mockStatic(ImageIO.class); + } + + @AfterEach + void afterEach() { + imageIOMockedStatic.close(); + } + + @Test + @DisplayName("accept should finalize the video") + void accept() throws IOException { + when(event.getContext()).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + when(testData.getVideoPath()).thenReturn(path); + when(testData.getEncoders()).thenReturn(encoders); + when(encoders.get(path)).thenReturn(encoder); + when(testData.getFrameNumber()).thenReturn(123); + + videoFinalizer.accept(event); + + verify(encoder).finish(); + verifyNoMoreInteractions(encoder); + } + + @Test + @DisplayName("accept should finalize the video adding the no video png if there are no frames") + void acceptNoVideoPng() throws IOException, URISyntaxException { + when(event.getContext()).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + when(testData.getVideoPath()).thenReturn(path); + when(testData.getEncoders()).thenReturn(encoders); + when(encoders.get(path)).thenReturn(encoder); + when(testData.getFrameNumber()).thenReturn(0); + + imageIOMockedStatic.when(() -> ImageIO.read(urlArgumentCaptor.capture())).thenReturn(bufferedImage); + + videoFinalizer.accept(event); + + assertEquals("no-video.png", Path.of(urlArgumentCaptor.getValue().toURI()).getFileName().toString()); + + verify(encoder).encodeImage(bufferedImage); + verify(encoder).finish(); + verifyNoMoreInteractions(encoder); + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumerTest.java new file mode 100644 index 00000000..c25559c2 --- /dev/null +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/events/video/VideoInitConsumerTest.java @@ -0,0 +1,75 @@ +package io.github.giulong.spectrum.utils.events.video; + +import io.github.giulong.spectrum.pojos.events.Event; +import io.github.giulong.spectrum.types.TestData; +import org.jcodec.api.awt.AWTSequenceEncoder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.nio.file.Path; +import java.util.Map; + +import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; +import static org.mockito.Mockito.*; + +class VideoInitConsumerTest { + + private MockedStatic awtSequenceEncoderMockedStatic; + + @Mock + private Event event; + + @Mock + private ExtensionContext context; + + @Mock + private ExtensionContext.Store store; + + @Mock + private Path path; + + @Mock + private AWTSequenceEncoder encoder; + + @Mock + private Map encoders; + + @Mock + private TestData testData; + + @InjectMocks + private VideoInitConsumer videoInitConsumer; + + @BeforeEach + void beforeEach() { + awtSequenceEncoderMockedStatic = mockStatic(AWTSequenceEncoder.class); + } + + @AfterEach + void afterEach() { + awtSequenceEncoderMockedStatic.close(); + } + + @Test + @DisplayName("init should init the needed fields") + void init() { + when(event.getContext()).thenReturn(context); + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); + when(testData.getVideoPath()).thenReturn(path); + when(testData.getEncoders()).thenReturn(encoders); + + awtSequenceEncoderMockedStatic.when(() -> AWTSequenceEncoder.createSequenceEncoder(path.toFile(), 1)).thenReturn(encoder); + + videoInitConsumer.accept(event); + + verify(encoders).put(path, encoder); + } +} diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumerTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumerTest.java index 1ce1dc92..6a1447f0 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumerTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumerTest.java @@ -1,91 +1,68 @@ package io.github.giulong.spectrum.utils.web_driver_events; import io.github.giulong.spectrum.enums.Frame; -import io.github.giulong.spectrum.types.TestData; -import io.github.giulong.spectrum.utils.FileUtils; +import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.utils.HtmlUtils; import io.github.giulong.spectrum.utils.Reflections; -import io.github.giulong.spectrum.utils.StatefulExtentTest; +import io.github.giulong.spectrum.utils.events.EventsDispatcher; import io.github.giulong.spectrum.utils.video.Video; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.*; -import org.openqa.selenium.TakesScreenshot; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.InjectMocks; +import org.mockito.Mock; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Map; import static io.github.giulong.spectrum.enums.Frame.AUTO_AFTER; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.pojos.Screenshot.SCREENSHOT; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import static org.openqa.selenium.OutputType.BYTES; class ScreenshotConsumerTest { - private static MockedStatic filesMockedStatic; - - @Mock - private FileUtils fileUtils; - @Mock - private WebDriverEvent webDriverEvent; + private HtmlUtils htmlUtils; @Mock - private TestData testData; + private EventsDispatcher eventsDispatcher; @Mock - private Path screenshotFolderPath; + private WebDriverEvent webDriverEvent; @Mock - private Path resolvedPath; + private ExtensionContext context; @Mock - private TakesScreenshot driver; + private Screenshot screenshot; @Mock private Video video; - @Mock - private StatefulExtentTest statefulExtentTest; - - @Captor - private ArgumentCaptor byteArgumentCaptor; - @InjectMocks private ScreenshotConsumer screenshotConsumer = new ScreenshotConsumer(ScreenshotConsumer.builder()); @BeforeEach void beforeEach() { - Reflections.setField("fileUtils", screenshotConsumer, fileUtils); - - filesMockedStatic = mockStatic(Files.class); - } - - @AfterEach - void afterEach() { - filesMockedStatic.close(); + Reflections.setField("htmlUtils", screenshotConsumer, htmlUtils); + Reflections.setField("eventsDispatcher", screenshotConsumer, eventsDispatcher); } @Test @DisplayName("accept should record the screenshot") void accept() { final Frame frame = AUTO_AFTER; - final String fileName = "fileName"; - when(testData.getScreenshotFolderPath()).thenReturn(screenshotFolderPath); - when(screenshotFolderPath.resolve(fileName)).thenReturn(resolvedPath); when(video.shouldRecord(eq(frame))).thenReturn(true); - when(driver.getScreenshotAs(BYTES)).thenReturn(new byte[]{1, 2, 3}); when(webDriverEvent.getFrame()).thenReturn(frame); - - when(fileUtils.getScreenshotNameFrom(frame, statefulExtentTest)).thenReturn(fileName); + when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); screenshotConsumer.accept(webDriverEvent); - filesMockedStatic.verify(() -> Files.write(eq(resolvedPath), byteArgumentCaptor.capture())); - assertArrayEquals(new byte[]{1, 2, 3}, byteArgumentCaptor.getValue()); + verify(eventsDispatcher).fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -98,7 +75,6 @@ void acceptShouldNotRecord() { screenshotConsumer.accept(webDriverEvent); - filesMockedStatic.verifyNoInteractions(); - verifyNoInteractions(driver); + verifyNoInteractions(eventsDispatcher); } } From 3541a74883967a1323a0854e8c03c7634d579a30 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sun, 20 Jul 2025 19:15:40 +0200 Subject: [PATCH 02/14] refactor: leveraging screenshot byte array data instead of reading from fs when inlining the html report --- .../giulong/spectrum/SpectrumEntity.java | 27 ++++++---- .../resolvers/TestDataResolver.java | 6 --- .../giulong/spectrum/pojos/Screenshot.java | 3 -- .../giulong/spectrum/types/TestData.java | 1 - .../spectrum/utils/ContextManager.java | 5 ++ .../giulong/spectrum/utils/FileUtils.java | 4 ++ .../giulong/spectrum/utils/HtmlUtils.java | 9 ++-- .../web_driver_events/ScreenshotConsumer.java | 3 ++ .../giulong/spectrum/SpectrumEntityTest.java | 52 ++++++++++++++++--- .../resolvers/TestDataResolverTest.java | 15 ------ .../giulong/spectrum/utils/FileUtilsTest.java | 15 ++++++ .../giulong/spectrum/utils/HtmlUtilsTest.java | 33 ++++++++---- .../ScreenshotConsumerTest.java | 13 +++++ 13 files changed, 131 insertions(+), 55 deletions(-) diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java b/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java index dd748639..de49ec77 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java @@ -42,6 +42,7 @@ public abstract class SpectrumEntity, Data> { public static final String HASH_ALGORITHM = "SHA-256"; + private final ContextManager contextManager = ContextManager.getInstance(); private final FileUtils fileUtils = FileUtils.getInstance(); private final HtmlUtils htmlUtils = HtmlUtils.getInstance(); @@ -177,28 +178,34 @@ public T screenshotFail(final String msg) { * * @param msg the message to log * @param status the log's status - * @return the generated screenshot */ @SneakyThrows - public Media addScreenshotToReport(final String msg, final Status status) { + public void addScreenshotToReport(final String msg, final Status status) { final ExtensionContext context = testContext.get(EXTENSION_CONTEXT, ExtensionContext.class); final Screenshot screenshot = htmlUtils.buildScreenshotFrom(context); eventsDispatcher.fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); - Files.write(screenshot.getPath(), screenshot.getData()); - final Media media = createScreenCaptureFromPath(screenshot.getPath().toString()).build(); + final String name = screenshot.getName(); + final String nameWithoutExtension = fileUtils.removeExtensionFrom(name); + final String extension = fileUtils.getExtensionWithDotOf(name); + final Path path = Files.createTempFile(nameWithoutExtension, extension); + + path.toFile().deleteOnExit(); + contextManager.getScreenshots().put(path.getFileName().toString(), screenshot); + Files.write(path, screenshot.getData()); + + final Media media = createScreenCaptureFromPath(path.toString()).build(); if (msg == null) { statefulExtentTest.getCurrentNode().log(status, (String) null, media); - } else { - final int frameNumber = configuration.getVideo().getAndIncrementFrameNumberFor(testData, MANUAL); - final String tag = htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message"); - - statefulExtentTest.getCurrentNode().log(status, tag, media); + return; } - return media; + final int frameNumber = configuration.getVideo().getAndIncrementFrameNumberFor(testData, MANUAL); + final String tag = htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message"); + + statefulExtentTest.getCurrentNode().log(status, tag, media); } /** diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolver.java b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolver.java index ef3a6262..03b590ca 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolver.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolver.java @@ -41,7 +41,6 @@ public TestData resolveParameter(final ParameterContext arg0, final ExtensionCon final String displayName = fileUtils.sanitize(joinTestDisplayNamesIn(context)); final String testId = buildTestIdFrom(className, displayName); final String fileName = fileUtils.removeExtensionFrom(extent.getFileName()); - final Path screenshotFolderPath = getScreenshotFolderPathForCurrentTest(reportFolder, fileName, classDisplayName, displayName); final Path videoPath = getVideoPathForCurrentTest(configuration.getVideo().isDisabled(), reportFolder, fileName, classDisplayName, displayName); final TestData testData = TestData .builder() @@ -50,7 +49,6 @@ public TestData resolveParameter(final ParameterContext arg0, final ExtensionCon .classDisplayName(classDisplayName) .displayName(displayName) .testId(testId) - .screenshotFolderPath(screenshotFolderPath) .videoPath(videoPath) .build(); @@ -79,10 +77,6 @@ public static String joinTestDisplayNamesIn(final ExtensionContext context) { return String.join(" ", displayNames.reversed()); } - Path getScreenshotFolderPathForCurrentTest(final String reportsFolder, final String extentFileName, final String className, final String methodName) { - return fileUtils.deleteContentOf(Path.of(reportsFolder, extentFileName, "screenshots", className, methodName).toAbsolutePath()); - } - Path getVideoPathForCurrentTest(final boolean disabled, final String reportsFolder, final String extentFileName, final String className, final String methodName) { if (disabled) { log.trace("Video disabled: avoiding video folder creation"); diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java b/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java index 1f6eb061..a4882cb2 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/pojos/Screenshot.java @@ -3,8 +3,6 @@ import lombok.Builder; import lombok.Getter; -import java.nio.file.Path; - @Getter @Builder public class Screenshot { @@ -12,6 +10,5 @@ public class Screenshot { public static final String SCREENSHOT = "screenshot"; private String name; - private Path path; private byte[] data; } diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java b/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java index a0f35ceb..622e3e92 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/types/TestData.java @@ -17,7 +17,6 @@ public class TestData { private String methodName; private String classDisplayName; private String testId; - private Path screenshotFolderPath; private Path videoPath; @Builder.Default diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/ContextManager.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/ContextManager.java index c5654be4..1a3514e2 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/ContextManager.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/ContextManager.java @@ -1,5 +1,7 @@ package io.github.giulong.spectrum.utils; +import io.github.giulong.spectrum.pojos.Screenshot; +import lombok.Getter; import lombok.NoArgsConstructor; import org.junit.jupiter.api.extension.ExtensionContext; @@ -19,6 +21,9 @@ public static ContextManager getInstance() { private final Map testContexts = new ConcurrentHashMap<>(); + @Getter + private final Map screenshots = new ConcurrentHashMap<>(); + public TestContext initFor(final ExtensionContext context) { return initFor(context, new TestContext()); } diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java index 95abffaa..e9dc4a9b 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/FileUtils.java @@ -61,6 +61,10 @@ public String interpolateTimestampFrom(final String value) { return value.replaceAll(TIMESTAMP_TO_REPLACE, LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern))); } + public String getExtensionWithDotOf(final String fileName) { + return fileName.substring(Math.max(0, fileName.lastIndexOf("."))); + } + public String getExtensionOf(final String fileName) { return fileName.substring(fileName.lastIndexOf(".") + 1); } diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java index 324db93b..6fb506e1 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/HtmlUtils.java @@ -19,7 +19,6 @@ import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.DRIVER; import static io.github.giulong.spectrum.extensions.resolvers.StatefulExtentTestResolver.STATEFUL_EXTENT_TEST; -import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; import static java.util.regex.Pattern.DOTALL; import static lombok.AccessLevel.PRIVATE; import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; @@ -34,8 +33,11 @@ public class HtmlUtils implements SessionHook { private static final Pattern IMAGE_TAG = Pattern.compile("
\\s*
\\s*[^\"]*)\".*?
\\s*
", DOTALL); private static final Base64.Encoder ENCODER = Base64.getEncoder(); private static final String SRC = "src"; + private final FreeMarkerWrapper freeMarkerWrapper = FreeMarkerWrapper.getInstance(); + private final ContextManager contextManager = ContextManager.getInstance(); private final FileUtils fileUtils = FileUtils.getInstance(); + private String videoTemplate; private String divTemplate; private String frameTemplate; @@ -69,13 +71,11 @@ public Screenshot buildScreenshotFrom(final ExtensionContext context) { final ExtensionContext.Store store = context.getStore(GLOBAL); final StatefulExtentTest statefulExtentTest = store.get(STATEFUL_EXTENT_TEST, StatefulExtentTest.class); final String fileName = fileUtils.buildScreenshotNameFrom(statefulExtentTest); - final TestData testData = store.get(TEST_DATA, TestData.class); final TakesScreenshot driver = (TakesScreenshot) store.get(DRIVER, WebDriver.class); return Screenshot .builder() .name(fileName) - .path(testData.getScreenshotFolderPath().resolve(fileName)) .data(driver.getScreenshotAs(BYTES)) .build(); } @@ -83,11 +83,12 @@ public Screenshot buildScreenshotFrom(final ExtensionContext context) { @SneakyThrows public String inlineImagesOf(final String html) { final Matcher matcher = IMAGE_TAG.matcher(html); + final Map screenshots = contextManager.getScreenshots(); String inlineHtml = html; while (matcher.find()) { final String src = matcher.group(SRC); - final byte[] bytes = Files.readAllBytes(Path.of(src)); + final byte[] bytes = screenshots.get(Path.of(src).getFileName().toString()).getData(); final String encoded = ENCODER.encodeToString(bytes); final String replacement = freeMarkerWrapper.interpolate(this.inlineImageTemplate, Map.of("encoded", encoded)); diff --git a/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java index bf5c4faa..ddd68d74 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/web_driver_events/ScreenshotConsumer.java @@ -2,6 +2,7 @@ import io.github.giulong.spectrum.enums.Frame; import io.github.giulong.spectrum.pojos.Screenshot; +import io.github.giulong.spectrum.utils.ContextManager; import io.github.giulong.spectrum.utils.HtmlUtils; import io.github.giulong.spectrum.utils.events.EventsDispatcher; import io.github.giulong.spectrum.utils.video.Video; @@ -18,6 +19,7 @@ @SuperBuilder public class ScreenshotConsumer extends WebDriverEventConsumer { + private final ContextManager contextManager = ContextManager.getInstance(); private final HtmlUtils htmlUtils = HtmlUtils.getInstance(); private final EventsDispatcher eventsDispatcher = EventsDispatcher.getInstance(); @@ -31,6 +33,7 @@ public void accept(final WebDriverEvent webDriverEvent) { if (video.shouldRecord(frame)) { final Screenshot screenshot = htmlUtils.buildScreenshotFrom(context); + contextManager.getScreenshots().put(screenshot.getName(), screenshot); eventsDispatcher.fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); log.trace("Recording frame {} for event '{}'", frame, webDriverEvent.getMessage()); diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java index bf7d6d5e..352366df 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java @@ -56,12 +56,18 @@ class SpectrumEntityTest { private MockedStatic mediaEntityBuilderMockedStatic; private MockedStatic messageDigestMockedStatic; + private final String screenshotName = "screenshotName"; + private final String nameWithoutExtension = "nameWithoutExtension"; + private final String extension = "extension"; private final String msg = "msg"; private final String tag = "tag"; private final int frameNumber = 123; private final byte[] bytes = new byte[]{1, 2, 3}; private final byte[] digest = new byte[]{4, 5, 6}; + @Mock + private ContextManager contextManager; + @Mock private WebDriverWait downloadWait; @@ -128,6 +134,9 @@ class SpectrumEntityTest { @Mock private Screenshot screenshot; + @Mock + private Map screenshots; + @Mock private MessageDigest messageDigest; @@ -142,6 +151,7 @@ class SpectrumEntityTest { @BeforeEach void beforeEach() { + Reflections.setField("contextManager", spectrumEntity, contextManager); Reflections.setField("configuration", spectrumEntity, configuration); Reflections.setField("htmlUtils", spectrumEntity, htmlUtils); Reflections.setField("fileUtils", spectrumEntity, fileUtils); @@ -165,7 +175,6 @@ void afterEach() { private void addScreenshotToReportStubs() { when(testContext.get(EXTENSION_CONTEXT, ExtensionContext.class)).thenReturn(context); when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); - when(screenshot.getPath()).thenReturn(path); when(statefulExtentTest.getCurrentNode()).thenReturn(extentTest); when(configuration.getVideo()).thenReturn(video); @@ -173,6 +182,16 @@ private void addScreenshotToReportStubs() { when(htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message")).thenReturn(tag); when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); when(mediaEntityBuilder.build()).thenReturn(media); + + when(screenshot.getName()).thenReturn(screenshotName); + when(fileUtils.removeExtensionFrom(screenshotName)).thenReturn(nameWithoutExtension); + when(fileUtils.getExtensionWithDotOf(screenshotName)).thenReturn(extension); + + filesMockedStatic.when(() -> Files.createTempFile(nameWithoutExtension, extension)).thenReturn(path); + when(path.toFile()).thenReturn(file); + when(path.getFileName()).thenReturn(path); + + when(contextManager.getScreenshots()).thenReturn(screenshots); } @Test @@ -223,12 +242,21 @@ void hover() { void screenshot() { when(testContext.get(EXTENSION_CONTEXT, ExtensionContext.class)).thenReturn(context); when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); - when(screenshot.getPath()).thenReturn(path); when(statefulExtentTest.getCurrentNode()).thenReturn(extentTest); when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); when(mediaEntityBuilder.build()).thenReturn(media); + when(screenshot.getName()).thenReturn(screenshotName); + when(fileUtils.removeExtensionFrom(screenshotName)).thenReturn(nameWithoutExtension); + when(fileUtils.getExtensionWithDotOf(screenshotName)).thenReturn(extension); + + filesMockedStatic.when(() -> Files.createTempFile(nameWithoutExtension, extension)).thenReturn(path); + when(path.toFile()).thenReturn(file); + when(path.getFileName()).thenReturn(path); + + when(contextManager.getScreenshots()).thenReturn(screenshots); + assertEquals(spectrumEntity, spectrumEntity.screenshot()); verify(extentTest).log(eq(INFO), (String) eq(null), any()); @@ -283,20 +311,30 @@ void addScreenshotToReport() { when(testContext.get(EXTENSION_CONTEXT, ExtensionContext.class)).thenReturn(context); when(htmlUtils.buildScreenshotFrom(context)).thenReturn(screenshot); - when(screenshot.getPath()).thenReturn(path); when(statefulExtentTest.getCurrentNode()).thenReturn(extentTest); when(configuration.getVideo()).thenReturn(video); when(video.getAndIncrementFrameNumberFor(testData, MANUAL)).thenReturn(frameNumber); when(htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message")).thenReturn(tag); + + when(screenshot.getName()).thenReturn(screenshotName); + when(fileUtils.removeExtensionFrom(screenshotName)).thenReturn(nameWithoutExtension); + when(fileUtils.getExtensionWithDotOf(screenshotName)).thenReturn(extension); + + filesMockedStatic.when(() -> Files.createTempFile(nameWithoutExtension, extension)).thenReturn(path); + when(path.toFile()).thenReturn(file); + when(path.getFileName()).thenReturn(path); + + when(contextManager.getScreenshots()).thenReturn(screenshots); + when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); when(mediaEntityBuilder.build()).thenReturn(media); - final Media actual = spectrumEntity.addScreenshotToReport(msg, status); - - assertEquals(media, actual); + spectrumEntity.addScreenshotToReport(msg, status); + verify(file).deleteOnExit(); + verify(screenshots).put(path.toString(), screenshot); verify(eventsDispatcher).fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, screenshot)); - verify(extentTest).log(status, tag, actual); + verify(extentTest).log(status, tag, media); verifyNoMoreInteractions(eventsDispatcher); } diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolverTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolverTest.java index c64893d8..83aef72c 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolverTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolverTest.java @@ -124,11 +124,6 @@ void resolveParameter() throws NoSuchMethodException { when(fileUtils.sanitize(classDisplayName)).thenReturn(sanitizedClassDisplayName); when(fileUtils.sanitize(displayName)).thenReturn(sanitizedDisplayName); - // getScreenshotFolderPathForCurrentTest - when(fileUtils.deleteContentOf( - Path.of(REPORTS_FOLDER, fileNameWithoutExtension, "screenshots", sanitizedClassDisplayName, sanitizedDisplayName).toAbsolutePath())) - .thenReturn(path); - // getVideoPathForCurrentTest when(fileUtils.deleteContentOf( Path.of(REPORTS_FOLDER, fileNameWithoutExtension, "videos", sanitizedClassDisplayName, sanitizedDisplayName).toAbsolutePath())) @@ -152,7 +147,6 @@ void resolveParameter() throws NoSuchMethodException { when(testDataBuilder.classDisplayName(sanitizedClassDisplayName)).thenReturn(testDataBuilder); when(testDataBuilder.displayName(sanitizedDisplayName)).thenReturn(testDataBuilder); when(testDataBuilder.testId(testId)).thenReturn(testDataBuilder); - when(testDataBuilder.screenshotFolderPath(path)).thenReturn(testDataBuilder); when(testDataBuilder.videoPath(pathArgumentCaptor.capture())).thenReturn(testDataBuilder); when(testDataBuilder.build()).thenReturn(testData); @@ -163,15 +157,6 @@ void resolveParameter() throws NoSuchMethodException { verify(contextManager).put(context, TEST_DATA, actual); } - @Test - @DisplayName("getScreenshotFolderPathForCurrentTest should return the path for the current test and create the dirs") - void getScreenshotFolderPathForCurrentTest() { - final String extentFileName = "extentFileName"; - - when(fileUtils.deleteContentOf(Path.of(REPORTS_FOLDER, extentFileName, "screenshots", CLASS_NAME, METHOD_NAME).toAbsolutePath())).thenReturn(path); - assertEquals(path, testDataResolver.getScreenshotFolderPathForCurrentTest(REPORTS_FOLDER, extentFileName, CLASS_NAME, METHOD_NAME)); - } - @Test @DisplayName("getVideoPathForCurrentTest should return the path for the current test and create the directories") void getVideoPathForCurrentTest() { diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java index c8d3a28f..9dbe8613 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/FileUtilsTest.java @@ -103,6 +103,21 @@ static Stream fileNamesProvider() { ); } + @DisplayName("getExtensionWithDotOf should return the extension with dot of the provided fileName") + @ParameterizedTest(name = "with fileName {0} we expect {1}") + @MethodSource("getExtensionWithDotOfValuesProvider") + void getExtensionWithDotOf(final String fileName, final String expected) { + assertEquals(expected, fileUtils.getExtensionWithDotOf(fileName)); + } + + static Stream getExtensionWithDotOfValuesProvider() { + return Stream.of( + arguments("fileName.abc", ".abc"), + arguments("fileName", "fileName"), + arguments("fileName.", ".") + ); + } + @DisplayName("getExtensionOf should return the extension of the provided fileName") @ParameterizedTest(name = "with fileName {0} we expect {1}") @MethodSource("getExtensionOfValuesProvider") diff --git a/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java index 7a551d85..615940e3 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/utils/HtmlUtilsTest.java @@ -19,7 +19,6 @@ import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.DRIVER; import static io.github.giulong.spectrum.extensions.resolvers.StatefulExtentTestResolver.STATEFUL_EXTENT_TEST; -import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.matchesPattern; @@ -37,6 +36,9 @@ class HtmlUtilsTest { private final String testId = "testId"; + @Mock + private ContextManager contextManager; + @Mock private Screenshot.ScreenshotBuilder screenshotBuilder; @@ -55,6 +57,12 @@ class HtmlUtilsTest { @Mock private Screenshot screenshot; + @Mock + private Screenshot screenshot2; + + @Mock + private Map screenshots; + @Mock private Path path; @@ -85,6 +93,7 @@ void beforeEach() { filesMockedStatic = mockStatic(Files.class); screenshotMockedStatic = mockStatic(Screenshot.class); + Reflections.setField("contextManager", htmlUtils, contextManager); Reflections.setField("freeMarkerWrapper", htmlUtils, freeMarkerWrapper); Reflections.setField("fileUtils", htmlUtils, fileUtils); } @@ -170,15 +179,11 @@ void buildScreenshotFrom() { when(context.getStore(GLOBAL)).thenReturn(store); when(store.get(STATEFUL_EXTENT_TEST, StatefulExtentTest.class)).thenReturn(statefulExtentTest); when(fileUtils.buildScreenshotNameFrom(statefulExtentTest)).thenReturn(screenshotName); - when(store.get(TEST_DATA, TestData.class)).thenReturn(testData); - when(testData.getScreenshotFolderPath()).thenReturn(path); - when(path.resolve(screenshotName)).thenReturn(path2); when(store.get(DRIVER, WebDriver.class)).thenReturn(driver); when(((TakesScreenshot) driver).getScreenshotAs(BYTES)).thenReturn(data); when(Screenshot.builder()).thenReturn(screenshotBuilder); when(screenshotBuilder.name(screenshotName)).thenReturn(screenshotBuilder); - when(screenshotBuilder.path(path2)).thenReturn(screenshotBuilder); when(screenshotBuilder.data(byteArrayArgumentCaptor.capture())).thenReturn(screenshotBuilder); when(screenshotBuilder.build()).thenReturn(screenshot); @@ -190,10 +195,10 @@ void buildScreenshotFrom() { @DisplayName("inline should call both the inlineImagesOf and inlineVideosOf on the provided html and return the one with all of those replaced") void inline() throws IOException { final String report = "abc