diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 754bd26d..09bd0996 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -16,5 +16,5 @@ # under the License. wrapperVersion=3.3.2 distributionType=bin -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar diff --git a/pom.xml b/pom.xml index 31a37104..afa4a41f 100644 --- a/pom.xml +++ b/pom.xml @@ -47,12 +47,12 @@ true 5.13.4 - 5.18.0 + 5.19.0 2.19.2 8.12.6 4.38.0 2.0.17 - 4.34.0 + 4.35.0 @@ -183,7 +183,7 @@ io.appium java-client - 9.5.0 + 10.0.0 @@ -227,7 +227,7 @@ com.slack.api slack-api-client - 1.45.3 + 1.45.4 @@ -323,7 +323,7 @@ com.puppycrawl.tools checkstyle - 10.26.1 + 11.0.0 com.github.sevntu-checkstyle @@ -485,7 +485,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.1 + 1.7.2 true ossrh diff --git a/spectrum/pom.xml b/spectrum/pom.xml index 0a9e9be5..7fed8853 100644 --- a/spectrum/pom.xml +++ b/spectrum/pom.xml @@ -328,7 +328,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.11.3 -Xdoclint:none 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..35213ba4 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/SpectrumEntity.java @@ -13,6 +13,7 @@ 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; @@ -28,10 +29,15 @@ 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 io.github.giulong.spectrum.extensions.resolvers.DriverResolver.DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.utils.web_driver_events.ScreenshotConsumer.SCREENSHOT; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; import static org.openqa.selenium.OutputType.BYTES; @Slf4j @@ -39,6 +45,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(); @@ -174,27 +181,27 @@ 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) { - final String fileName = fileUtils.getScreenshotNameFrom(MANUAL, statefulExtentTest); - final Path screenshotPath = testData.getScreenshotFolderPath().resolve(fileName); + public void addScreenshotToReport(final String msg, final Status status) { + final ExtensionContext context = testContext.get(EXTENSION_CONTEXT, ExtensionContext.class); + final byte[] screenshot = ((TakesScreenshot) context.getStore(GLOBAL).get(DRIVER, WebDriver.class)).getScreenshotAs(BYTES); - 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(); + final Path path = fileUtils.writeTempFile("screenshot", ".png", screenshot); + final Media media = createScreenCaptureFromPath(path.toString()).build(); - if (msg == null) { - statefulExtentTest.getCurrentNode().log(status, (String) null, screenshot); - } else { - final int frameNumber = configuration.getVideo().getAndIncrementFrameNumberFor(testData, MANUAL); - final String tag = htmlUtils.buildFrameTagFor(frameNumber, msg, testData, "screenshot-message"); + contextManager.getScreenshots().put(path, screenshot); - statefulExtentTest.getCurrentNode().log(status, tag, screenshot); + if (msg == null) { + statefulExtentTest.getCurrentNode().log(status, (String) null, media); + return; } - return screenshot; + 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/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/resolvers/TestDataResolver.java b/spectrum/src/main/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolver.java index ef3a6262..7c27df9d 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 @@ -1,7 +1,6 @@ package io.github.giulong.spectrum.extensions.resolvers; 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.FileUtils; import lombok.extern.slf4j.Slf4j; @@ -14,8 +13,6 @@ import java.util.ArrayList; import java.util.List; -import static io.github.giulong.spectrum.extensions.resolvers.ConfigurationResolver.CONFIGURATION; -import static java.util.UUID.randomUUID; import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; @Slf4j @@ -30,19 +27,13 @@ public class TestDataResolver extends TypeBasedParameterResolver { public TestData resolveParameter(final ParameterContext arg0, final ExtensionContext context) { log.debug("Resolving {}", TEST_DATA); - final ExtensionContext.Store store = context.getStore(GLOBAL); - final Configuration configuration = context.getRoot().getStore(GLOBAL).get(CONFIGURATION, Configuration.class); - final Configuration.Extent extent = configuration.getExtent(); - final String reportFolder = extent.getReportFolder(); final Class clazz = context.getRequiredTestClass(); final String className = clazz.getSimpleName(); final String methodName = context.getRequiredTestMethod().getName(); final String classDisplayName = fileUtils.sanitize(getDisplayNameOf(clazz)); 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 Path videoPath = fileUtils.createTempFile("video", ".mp4"); final TestData testData = TestData .builder() .className(className) @@ -50,11 +41,10 @@ public TestData resolveParameter(final ParameterContext arg0, final ExtensionCon .classDisplayName(classDisplayName) .displayName(displayName) .testId(testId) - .screenshotFolderPath(screenshotFolderPath) .videoPath(videoPath) .build(); - store.put(TEST_DATA, testData); + context.getStore(GLOBAL).put(TEST_DATA, testData); contextManager.put(context, TEST_DATA, testData); return testData; } @@ -79,21 +69,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"); - return null; - } - - return fileUtils - .deleteContentOf(Path.of(reportsFolder, extentFileName, "videos", className, methodName).toAbsolutePath()) - .resolve(String.format("%s.mp4", randomUUID())); - } - static String transformInKebabCase(final String string) { return string.replaceAll("\\s", "-").toLowerCase(); } 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/internals/web_driver_listeners/EventsWebDriverListener.java b/spectrum/src/main/java/io/github/giulong/spectrum/internals/web_driver_listeners/EventsWebDriverListener.java index bbb392d9..eb0f8eec 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/internals/web_driver_listeners/EventsWebDriverListener.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/internals/web_driver_listeners/EventsWebDriverListener.java @@ -712,14 +712,14 @@ public void afterImplicitlyWait(final WebDriver.Timeouts timeouts, final Duratio @Override @Generated - public void beforeSetScriptTimeout(final WebDriver.Timeouts timeouts, final Duration duration) { - listenTo(AUTO_BEFORE, events.getBeforeSetScriptTimeout(), timeouts, duration); + public void beforeScriptTimeout(final WebDriver.Timeouts timeouts, final Duration duration) { + listenTo(AUTO_BEFORE, events.getBeforeScriptTimeout(), timeouts, duration); } @Override @Generated - public void afterSetScriptTimeout(final WebDriver.Timeouts timeouts, final Duration duration) { - listenTo(AUTO_AFTER, events.getAfterSetScriptTimeout(), timeouts, duration); + public void afterScriptTimeout(final WebDriver.Timeouts timeouts, final Duration duration) { + listenTo(AUTO_AFTER, events.getAfterScriptTimeout(), timeouts, duration); } @Override @@ -818,6 +818,126 @@ public void afterFullscreen(final WebDriver.Window window) { listenTo(AUTO_AFTER, events.getAfterFullscreen(), window); } + @Override + @Generated + public void beforeAnyTargetLocatorCall(final WebDriver.TargetLocator targetLocator, final Method method, final Object[] args) { + listenTo(AUTO_BEFORE, events.getBeforeAnyTargetLocatorCall(), targetLocator, method, Arrays.toString(args)); + } + + @Override + @Generated + public void afterAnyTargetLocatorCall(final WebDriver.TargetLocator targetLocator, final Method method, final Object[] args, final Object result) { + listenTo(AUTO_AFTER, events.getAfterAnyTargetLocatorCall(), targetLocator, method, Arrays.toString(args), result); + } + + @Override + @Generated + public void beforeFrame(final WebDriver.TargetLocator targetLocator, final int index) { + listenTo(AUTO_BEFORE, events.getBeforeFrame(), targetLocator, index); + } + + @Override + @Generated + public void afterFrame(final WebDriver.TargetLocator targetLocator, final int index, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterFrame(), targetLocator, index, driver); + } + + @Override + @Generated + public void beforeFrame(final WebDriver.TargetLocator targetLocator, final String nameOrId) { + listenTo(AUTO_BEFORE, events.getBeforeFrame(), targetLocator, nameOrId); + } + + @Override + @Generated + public void afterFrame(final WebDriver.TargetLocator targetLocator, final String nameOrId, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterFrame(), targetLocator, nameOrId, driver); + } + + @Override + @Generated + public void beforeFrame(final WebDriver.TargetLocator targetLocator, final WebElement frameElement) { + listenTo(AUTO_BEFORE, events.getBeforeFrame(), targetLocator, frameElement); + } + + @Override + @Generated + public void afterFrame(final WebDriver.TargetLocator targetLocator, final WebElement frameElement, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterFrame(), targetLocator, frameElement, driver); + } + + @Override + @Generated + public void beforeParentFrame(final WebDriver.TargetLocator targetLocator) { + listenTo(AUTO_BEFORE, events.getBeforeParentFrame(), targetLocator); + } + + @Override + @Generated + public void afterParentFrame(final WebDriver.TargetLocator targetLocator, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterParentFrame(), targetLocator, driver); + } + + @Override + @Generated + public void beforeWindow(final WebDriver.TargetLocator targetLocator, final String nameOrHandle) { + listenTo(AUTO_BEFORE, events.getBeforeWindow(), targetLocator, nameOrHandle); + } + + @Override + @Generated + public void afterWindow(final WebDriver.TargetLocator targetLocator, final String nameOrHandle, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterWindow(), targetLocator, nameOrHandle, driver); + } + + @Override + @Generated + public void beforeNewWindow(final WebDriver.TargetLocator targetLocator, final WindowType typeHint) { + listenTo(AUTO_BEFORE, events.getBeforeNewWindow(), targetLocator, typeHint); + } + + @Override + @Generated + public void afterNewWindow(final WebDriver.TargetLocator targetLocator, final WindowType typeHint, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterNewWindow(), targetLocator, typeHint, driver); + } + + @Override + @Generated + public void beforeDefaultContent(final WebDriver.TargetLocator targetLocator) { + listenTo(AUTO_BEFORE, events.getBeforeDefaultContent(), targetLocator); + } + + @Override + @Generated + public void afterDefaultContent(final WebDriver.TargetLocator targetLocator, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterDefaultContent(), targetLocator, driver); + } + + @Override + @Generated + public void beforeActiveElement(final WebDriver.TargetLocator targetLocator) { + listenTo(AUTO_BEFORE, events.getBeforeActiveElement(), targetLocator); + } + + @Override + @Generated + public void afterActiveElement(final WebDriver.TargetLocator targetLocator, final WebDriver driver) { + listenTo(AUTO_AFTER, events.getAfterActiveElement(), targetLocator, driver); + } + + @Override + @Generated + public void beforeAlert(final WebDriver.TargetLocator targetLocator) { + listenTo(AUTO_BEFORE, events.getBeforeAlert(), targetLocator); + } + + @Override + @Generated + public void afterAlert(final WebDriver.TargetLocator targetLocator, final Alert alert) { + listenTo(AUTO_AFTER, events.getAfterAlert(), targetLocator, alert); + } + boolean isSecured(final CharSequence... keysToSend) { final Matcher matcher = SECURED_PATTERN.matcher(keysToSend[0]); 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..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 @@ -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 @@ -14,12 +17,23 @@ public class TestData { private String methodName; private String classDisplayName; private String testId; - 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/Configuration.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/Configuration.java index ba328221..e4e3519e 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/Configuration.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/Configuration.java @@ -525,8 +525,8 @@ public static class Events { private Event afterAnyTimeoutsCall; private Event beforeImplicitlyWait; private Event afterImplicitlyWait; - private Event beforeSetScriptTimeout; - private Event afterSetScriptTimeout; + private Event beforeScriptTimeout; + private Event afterScriptTimeout; private Event beforePageLoadTimeout; private Event afterPageLoadTimeout; private Event beforeAnyWindowCall; @@ -543,6 +543,22 @@ public static class Events { private Event afterMaximize; private Event beforeFullscreen; private Event afterFullscreen; + private Event beforeAnyTargetLocatorCall; + private Event afterAnyTargetLocatorCall; + private Event beforeFrame; + private Event afterFrame; + private Event beforeParentFrame; + private Event afterParentFrame; + private Event beforeWindow; + private Event afterWindow; + private Event beforeNewWindow; + private Event afterNewWindow; + private Event beforeDefaultContent; + private Event afterDefaultContent; + private Event beforeActiveElement; + private Event afterActiveElement; + private Event beforeAlert; + private Event afterAlert; } @Getter 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..e4d465b9 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,8 +1,10 @@ package io.github.giulong.spectrum.utils; +import lombok.Getter; import lombok.NoArgsConstructor; import org.junit.jupiter.api.extension.ExtensionContext; +import java.nio.file.Path; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -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/ExtentReporter.java b/spectrum/src/main/java/io/github/giulong/spectrum/utils/ExtentReporter.java index 9deac28c..b663075b 100644 --- a/spectrum/src/main/java/io/github/giulong/spectrum/utils/ExtentReporter.java +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/ExtentReporter.java @@ -34,11 +34,11 @@ import static com.aventstack.extentreports.markuputils.MarkupHelper.createLabel; import static io.github.giulong.spectrum.extensions.resolvers.StatefulExtentTestResolver.STATEFUL_EXTENT_TEST; import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.*; -import static lombok.AccessLevel.PROTECTED; +import static lombok.AccessLevel.PRIVATE; @Slf4j -@NoArgsConstructor(access = PROTECTED) @Getter +@NoArgsConstructor(access = PRIVATE) public class ExtentReporter implements SessionHook, CanProduceMetadata { private static final ExtentReporter INSTANCE = new ExtentReporter(); 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..bae2df37 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; @@ -20,7 +19,6 @@ import java.util.stream.Stream; import static java.util.Comparator.reverseOrder; -import static java.util.UUID.randomUUID; import static lombok.AccessLevel.PRIVATE; @Slf4j @@ -72,7 +70,7 @@ public String removeExtensionFrom(final String fileName) { @SneakyThrows public Path delete(final Path path) { - if (!Files.exists(path)) { + if (Files.notExists(path)) { log.debug("Avoid deleting non-existing path '{}'", path); return path; } @@ -130,7 +128,17 @@ 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()); + @SneakyThrows + public Path createTempFile(final String prefix, final String suffix) { + final Path path = Files.createTempFile(prefix, suffix); + path.toFile().deleteOnExit(); + + log.debug("Created file {}", path); + return path; + } + + @SneakyThrows + public Path writeTempFile(final String prefix, final String suffix, final byte[] data) { + return Files.write(createTempFile(prefix, suffix), data); } } 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..6640963f 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 @@ -25,8 +25,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; @@ -59,11 +62,12 @@ public String buildFrameTagFor(final int number, final String content, final Tes @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)); 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/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..20ebf22c --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoConsumer.java @@ -0,0 +1,112 @@ +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 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.utils.web_driver_events.ScreenshotConsumer.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 byte[] screenshot = (byte[]) 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)), dimension)); + } + + @SneakyThrows + protected boolean isNewFrame(final byte[] screenshot, final TestData testData) { + final byte[] digest = messageDigest.digest(screenshot); + + if (!Arrays.equals(testData.getLastFrameDigest(), digest)) { + testData.setLastFrameDigest(digest); + return true; + } + + log.trace("Discarding duplicate frame"); + 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..b601ac89 --- /dev/null +++ b/spectrum/src/main/java/io/github/giulong/spectrum/utils/events/video/VideoDynamicConsumer.java @@ -0,0 +1,51 @@ +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 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 byte[] 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..5f489e3e 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,43 @@ 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.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.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; +import java.util.Map; +import static io.github.giulong.spectrum.extensions.resolvers.DriverResolver.DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; import static org.openqa.selenium.OutputType.BYTES; @Slf4j @SuperBuilder public class ScreenshotConsumer extends WebDriverEventConsumer { - private final FileUtils fileUtils = FileUtils.getInstance(); + public static final String SCREENSHOT = "screenshot"; + + 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 byte[] screenshot = ((TakesScreenshot) context.getStore(GLOBAL).get(DRIVER, WebDriver.class)).getScreenshotAs(BYTES); + + 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..052f1ff4 100644 --- a/spectrum/src/main/resources/yaml/configuration.default.yaml +++ b/spectrum/src/main/resources/yaml/configuration.default.yaml @@ -417,9 +417,9 @@ drivers: message: Waiting for %2$s afterImplicitlyWait: message: Waited for %2$s - beforeSetScriptTimeout: + beforeScriptTimeout: message: Setting script timeout to %2$s - afterSetScriptTimeout: + afterScriptTimeout: message: Script timeout set to %2$s beforePageLoadTimeout: message: About to reach page load timeout of %2$s @@ -459,6 +459,38 @@ drivers: message: Setting fullscreen afterFullscreen: message: Window set to fullscreen + beforeAnyTargetLocatorCall: + message: Before target locator %1$s call + afterAnyTargetLocatorCall: + message: Result of target locator %1$s call is %2$s + beforeFrame: + message: Before frame %1$s %2$s + afterFrame: + message: After frame %1$s %2$s + beforeParentFrame: + message: Before parent frame %1$s + afterParentFrame: + message: After parent frame %1$s + beforeWindow: + message: Before window %1$s %2$s + afterWindow: + message: After window %1$s %2$s + beforeNewWindow: + message: Before new window %1$s %2$s + afterNewWindow: + message: After new window %1$s %2$s + beforeDefaultContent: + message: Before default content %1$s + afterDefaultContent: + message: After default content %1$s + beforeActiveElement: + message: Before active element %1$s + afterActiveElement: + message: After active element %1$s + beforeAlert: + message: Before alert %1$s + afterAlert: + message: After alert %1$s %2$s # Data models data: @@ -476,23 +508,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..23a527d2 100644 --- a/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java +++ b/spectrum/src/test/java/io/github/giulong/spectrum/SpectrumEntityTest.java @@ -7,21 +7,20 @@ import io.github.giulong.spectrum.interfaces.Shared; 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; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.*; -import org.openqa.selenium.By; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.*; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.WebDriverWait; @@ -33,13 +32,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.DriverResolver.DRIVER; +import static io.github.giulong.spectrum.extensions.resolvers.TestContextResolver.EXTENSION_CONTEXT; +import static io.github.giulong.spectrum.utils.web_driver_events.ScreenshotConsumer.SCREENSHOT; 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.*; import static org.openqa.selenium.OutputType.BYTES; @@ -51,13 +55,17 @@ class SpectrumEntityTest { private MockedStatic mediaEntityBuilderMockedStatic; private MockedStatic messageDigestMockedStatic; + private final String screenshot = "screenshot"; + private final String extension = ".png"; 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}; + @Mock + private ContextManager contextManager; + @Mock private WebDriverWait downloadWait; @@ -73,9 +81,6 @@ class SpectrumEntityTest { @Mock private StatefulExtentTest statefulExtentTest; - @Mock(extraInterfaces = TakesScreenshot.class) - private WebDriver webDriver; - @Mock private WebElement webElement; @@ -97,9 +102,6 @@ class SpectrumEntityTest { @Mock private Path path; - @Mock - private Path reportsFolder; - @Mock private Path downloadsFolderPath; @@ -109,11 +111,29 @@ 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 ExtensionContext.Store store; + + @Mock(extraInterfaces = TakesScreenshot.class) + private WebDriver driver; + + @Mock + private Media media; + + @Mock + private Map screenshots; @Mock private MessageDigest messageDigest; @@ -121,6 +141,9 @@ class SpectrumEntityTest { @Mock private TestData testData; + @Captor + private ArgumentCaptor byteArrayArgumentCaptor; + @Captor private ArgumentCaptor> functionArgumentCaptor; @@ -129,9 +152,11 @@ 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); + Reflections.setField("eventsDispatcher", spectrumEntity, eventsDispatcher); pathMockedStatic = mockStatic(Path.class); filesMockedStatic = mockStatic(Files.class); @@ -149,19 +174,22 @@ 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(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(DRIVER, WebDriver.class)).thenReturn(driver); + when(((TakesScreenshot) driver).getScreenshotAs(BYTES)).thenReturn(bytes); - 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); + + when(fileUtils.writeTempFile(eq(screenshot), eq(extension), byteArrayArgumentCaptor.capture())).thenReturn(path); + + when(contextManager.getScreenshots()).thenReturn(screenshots); } @Test @@ -210,16 +238,19 @@ 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(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); + + when(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(DRIVER, WebDriver.class)).thenReturn(driver); + when(((TakesScreenshot) driver).getScreenshotAs(BYTES)).thenReturn(bytes); + + when(fileUtils.writeTempFile(screenshot, extension, bytes)).thenReturn(path); + + when(contextManager.getScreenshots()).thenReturn(screenshots); assertEquals(spectrumEntity, spectrumEntity.screenshot()); @@ -273,23 +304,30 @@ 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(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(context.getStore(GLOBAL)).thenReturn(store); + when(store.get(DRIVER, WebDriver.class)).thenReturn(driver); + when(((TakesScreenshot) driver).getScreenshotAs(BYTES)).thenReturn(bytes); - final Media actual = spectrumEntity.addScreenshotToReport(msg, status); + when(fileUtils.writeTempFile(eq(screenshot), eq(extension), byteArrayArgumentCaptor.capture())).thenReturn(path); + + when(contextManager.getScreenshots()).thenReturn(screenshots); + + when(MediaEntityBuilder.createScreenCaptureFromPath(path.toString())).thenReturn(mediaEntityBuilder); + when(mediaEntityBuilder.build()).thenReturn(media); - assertEquals(screenshot, actual); + spectrumEntity.addScreenshotToReport(msg, status); - verify(extentTest).log(status, tag, actual); + assertArrayEquals(bytes, byteArrayArgumentCaptor.getValue()); + verify(screenshots).put(path, bytes); + verify(eventsDispatcher).fire(SCREENSHOT, SCREENSHOT, Map.of(EXTENSION_CONTEXT, context, SCREENSHOT, bytes)); + verify(extentTest).log(status, tag, media); + verifyNoMoreInteractions(eventsDispatcher); } @Test @@ -318,7 +356,7 @@ void waitForDownloadOf() { verify(downloadWait).until(functionArgumentCaptor.capture()); final Function function = functionArgumentCaptor.getValue(); - assertTrue(function.apply(webDriver)); + assertTrue(function.apply(driver)); } @Test @@ -333,7 +371,7 @@ void waitForDownloadOfNotYetDone() { verify(downloadWait).until(functionArgumentCaptor.capture()); final Function function = functionArgumentCaptor.getValue(); - assertFalse(function.apply(webDriver)); + assertFalse(function.apply(driver)); } @Test @@ -346,7 +384,7 @@ void waitForDownloadOfNotYetCreated() { verify(downloadWait).until(functionArgumentCaptor.capture()); final Function function = functionArgumentCaptor.getValue(); - assertFalse(function.apply(webDriver)); + assertFalse(function.apply(driver)); } @Test @@ -437,7 +475,7 @@ void upload() { @ParameterizedTest(name = "with list {0} we expect {1}") @MethodSource("isPresentProvider") void isPresent(final List webElements, final boolean expected) { - when(webDriver.findElements(by)).thenReturn(webElements); + when(driver.findElements(by)).thenReturn(webElements); assertEquals(expected, spectrumEntity.isPresent(by)); } @@ -453,7 +491,7 @@ static Stream isPresentProvider() { @ParameterizedTest(name = "with list {0} we expect {1}") @MethodSource("isNotPresentProvider") void isNotPresent(final List webElements, final boolean expected) { - when(webDriver.findElements(by)).thenReturn(webElements); + when(driver.findElements(by)).thenReturn(webElements); assertEquals(expected, spectrumEntity.isNotPresent(by)); } 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/resolvers/TestDataResolverTest.java b/spectrum/src/test/java/io/github/giulong/spectrum/extensions/resolvers/TestDataResolverTest.java index c64893d8..49b4485f 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 @@ -1,8 +1,9 @@ package io.github.giulong.spectrum.extensions.resolvers; import io.github.giulong.spectrum.types.TestData; -import io.github.giulong.spectrum.utils.*; -import io.github.giulong.spectrum.utils.video.Video; +import io.github.giulong.spectrum.utils.ContextManager; +import io.github.giulong.spectrum.utils.FileUtils; +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; @@ -16,22 +17,13 @@ import java.util.List; import java.util.Optional; -import static io.github.giulong.spectrum.extensions.resolvers.ConfigurationResolver.CONFIGURATION; import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.TEST_DATA; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; import static org.mockito.Mockito.*; class TestDataResolverTest { - private static final String UUID_REGEX = "([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})\\.mp4"; - private static final String CLASS_NAME = "className"; - private static final String METHOD_NAME = "methodName"; - private static final String REPORTS_FOLDER = "reportsFolder"; - private static MockedStatic testDataMockedStatic; @Mock @@ -61,30 +53,12 @@ class TestDataResolverTest { @Mock private ExtensionContext.Store store; - @Mock - private ExtensionContext.Store rootStore; - - @Mock - private Configuration configuration; - - @Mock - private Configuration.Extent extent; - @Mock private TestData.TestDataBuilder testDataBuilder; @Mock private TestData testData; - @Mock - private Video video; - - @Captor - private ArgumentCaptor pathArgumentCaptor; - - @Captor - private ArgumentCaptor stringArgumentCaptor; - @InjectMocks private TestDataResolver testDataResolver; @@ -112,39 +86,19 @@ void resolveParameter() throws NoSuchMethodException { final String displayName = "displayName"; final String sanitizedDisplayName = "sanitizedDisplayName"; final String testId = "string-sanitizeddisplayname"; - final String fileName = "fileName"; - final String fileNameWithoutExtension = "fileNameWithoutExtension"; // joinTestDisplayNamesIn when(context.getParent()).thenReturn(Optional.of(parentContext)); when(parentContext.getParent()).thenReturn(Optional.of(rootContext)); when(context.getDisplayName()).thenReturn(displayName); - when(fileUtils.removeExtensionFrom(fileName)).thenReturn(fileNameWithoutExtension); 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())) - .thenReturn(path); + when(fileUtils.createTempFile("video", ".mp4")).thenReturn(path); when(context.getStore(GLOBAL)).thenReturn(store); - when(context.getRoot()).thenReturn(rootContext); - when(rootContext.getStore(GLOBAL)).thenReturn(rootStore); - when(rootStore.get(CONFIGURATION, Configuration.class)).thenReturn(configuration); - when(configuration.getExtent()).thenReturn(extent); - when(extent.getReportFolder()).thenReturn(REPORTS_FOLDER); - when(extent.getFileName()).thenReturn(fileName); doReturn(String.class).when(context).getRequiredTestClass(); when(context.getRequiredTestMethod()).thenReturn(getClass().getDeclaredMethod(methodName)); - when(configuration.getVideo()).thenReturn(video); - when(video.isDisabled()).thenReturn(false); when(TestData.builder()).thenReturn(testDataBuilder); when(testDataBuilder.className(className)).thenReturn(testDataBuilder); @@ -152,8 +106,7 @@ 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.videoPath(path)).thenReturn(testDataBuilder); when(testDataBuilder.build()).thenReturn(testData); final TestData actual = testDataResolver.resolveParameter(parameterContext, context); @@ -163,33 +116,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() { - final String extentFileName = "extentFileName"; - - when(fileUtils.deleteContentOf(Path.of(REPORTS_FOLDER, extentFileName, "videos", CLASS_NAME, METHOD_NAME).toAbsolutePath())).thenReturn(path); - when(path.resolve(stringArgumentCaptor.capture())).thenReturn(path); - - assertEquals(path, testDataResolver.getVideoPathForCurrentTest(false, REPORTS_FOLDER, extentFileName, CLASS_NAME, METHOD_NAME)); - assertThat(stringArgumentCaptor.getValue(), matchesPattern(UUID_REGEX)); - } - - @Test - @DisplayName("getVideoPathForCurrentTest should return null if video is disabled") - void getVideoPathForCurrentTestDisabled() { - assertNull(testDataResolver.getVideoPathForCurrentTest(true, REPORTS_FOLDER, "extentFileName", CLASS_NAME, METHOD_NAME)); - } - @Test @DisplayName("getDisplayNameOf should return the @DisplayName value") void getDisplayNameOf() { 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..9b9ce610 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 @@ -1,11 +1,15 @@ package io.github.giulong.spectrum.utils; +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 org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; import java.io.File; import java.io.IOException; @@ -15,7 +19,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; @@ -25,8 +28,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 MockedStatic filesMockedStatic; @Mock private BasicFileAttributes basicFileAttributes; @@ -43,12 +45,19 @@ class FileUtilsTest { @Mock private File file; - @Mock - private StatefulExtentTest statefulExtentTest; - @InjectMocks private FileUtils fileUtils; + @BeforeEach + void beforeEach() { + filesMockedStatic = mockStatic(Files.class); + } + + @AfterEach + void afterEach() { + filesMockedStatic.close(); + } + @Test @DisplayName("getInstance should return the singleton") void getInstance() { @@ -132,51 +141,64 @@ static Stream removeExtensionFromValuesProvider() { ); } - @DisplayName("delete should delete the provided folder and return the reference to it") - @ParameterizedTest(name = "with value {0} which is existing? {1}") - @MethodSource("deleteValuesProvider") - void delete(final Path directory, final boolean existing) { - directory.toFile().deleteOnExit(); - assertEquals(existing, Files.exists(directory)); + @Test + @DisplayName("delete should do nothing if the provided folder doesn't exist and return the reference to it") + void deleteNotExisting() { + when(Files.notExists(path)).thenReturn(true); - assertEquals(directory, fileUtils.delete(directory)); + assertEquals(path, fileUtils.delete(path)); - assertFalse(Files.exists(directory)); + //noinspection resource + filesMockedStatic.verify(() -> Files.walk(path), never()); } - static Stream deleteValuesProvider() throws IOException { - return Stream.of( - arguments(Path.of("abc not existing"), false), - arguments(Files.createTempDirectory("downloadsFolder"), true)); + @Test + @DisplayName("delete should delete the provided folder and return the reference to it") + void delete() throws IOException { + final Path filePath = mock(); + + when(Files.notExists(path)).thenReturn(false); + when(Files.walk(path)).thenReturn(Stream.of(filePath)); + when(filePath.toFile()).thenReturn(file); + + assertEquals(path, fileUtils.delete(path)); + + verify(file).delete(); } @Test @DisplayName("overloaded delete that takes a file should delegate to the one taking its path") void deleteFile() throws IOException { - final File directory = Files.createTempDirectory("downloadsFolder").toFile(); - directory.deleteOnExit(); + final Path filePath = mock(); - assertEquals(directory.toPath(), fileUtils.delete(directory)); + when(file.toPath()).thenReturn(path); + when(Files.notExists(path)).thenReturn(false); + when(Files.walk(path)).thenReturn(Stream.of(filePath)); + when(filePath.toFile()).thenReturn(file); - assertFalse(directory.exists()); + assertEquals(path, fileUtils.delete(file)); + + verify(file).delete(); } + @Test @DisplayName("delete should delete the provided directory, recreate it, and return the reference to it") - @ParameterizedTest(name = "with value {0} which is existing? {1}") - @MethodSource("deleteValuesProvider") - void deleteContentOf(final Path directory, final boolean existing) { - directory.toFile().deleteOnExit(); - assertEquals(existing, Files.exists(directory)); + void deleteContentOf() throws IOException { + final Path filePath = mock(); - assertEquals(directory, fileUtils.deleteContentOf(directory)); + when(Files.createDirectories(path)).thenReturn(path); - assertTrue(Files.exists(directory)); + when(Files.notExists(path)).thenReturn(false); + when(Files.walk(path)).thenReturn(Stream.of(filePath)); + when(filePath.toFile()).thenReturn(file); + + assertEquals(path, fileUtils.deleteContentOf(path)); + verify(file).delete(); } @Test @DisplayName("write should write the provided content to a file in the provided path, creating the parent folders if needed") void write() { - final MockedStatic filesMockedStatic = mockStatic(Files.class); final String content = "content"; when(path.getParent()).thenReturn(parentPath); @@ -186,14 +208,11 @@ void write() { verify(file).mkdirs(); filesMockedStatic.verify(() -> Files.write(path, content.getBytes())); - - filesMockedStatic.close(); } @Test @DisplayName("write should write the provided content to a file in the provided string path, creating the parent folders if needed") void writeString() { - final MockedStatic filesMockedStatic = mockStatic(Files.class); final MockedStatic pathMockedStatic = mockStatic(Path.class); final String stringPath = "stringPath"; final String content = "content"; @@ -207,7 +226,6 @@ void writeString() { verify(file).mkdirs(); filesMockedStatic.verify(() -> Files.write(path, content.getBytes())); - filesMockedStatic.close(); pathMockedStatic.close(); } @@ -223,22 +241,40 @@ void sanitize() { @Test @DisplayName("getCreationTimeOf should return the creation time of the provided file") void getCreationTimeOf() throws IOException { - final MockedStatic filesMockedStatic = mockStatic(Files.class); - when(file.toPath()).thenReturn(path); when(Files.readAttributes(path, BasicFileAttributes.class)).thenReturn(basicFileAttributes); when(basicFileAttributes.creationTime()).thenReturn(creationTime); assertEquals(creationTime, fileUtils.getCreationTimeOf(file)); + } - filesMockedStatic.close(); + @Test + @DisplayName("createTempFile should create a temp file with the provided prefix, suffix, and delete it on exit") + void createTempFile() throws IOException { + final String prefix = "prefix"; + final String suffix = "suffix"; + + when(Files.createTempFile(prefix, suffix)).thenReturn(path); + when(path.toFile()).thenReturn(file); + + assertEquals(path, fileUtils.createTempFile(prefix, suffix)); + + verify(file).deleteOnExit(); } @Test - @DisplayName("getScreenshotNameFrom should return the name for the provided frame and display name") - void getScreenshotNameFrom() { - when(statefulExtentTest.getDisplayName()).thenReturn(DISPLAY_NAME); + @DisplayName("writeTempFile should create a temp file with the provided prefix, suffix, and content and delete it on exit") + void writeTempFile() throws IOException { + final String prefix = "prefix"; + final String suffix = "suffix"; + final byte[] data = new byte[]{1, 2, 3}; + + when(Files.createTempFile(prefix, suffix)).thenReturn(path); + when(path.toFile()).thenReturn(file); + when(Files.write(path, data)).thenReturn(path); + + assertEquals(path, fileUtils.writeTempFile(prefix, suffix, data)); - assertThat(fileUtils.getScreenshotNameFrom(AUTO_AFTER, statefulExtentTest), matchesPattern(UUID_REGEX)); + verify(file).deleteOnExit(); } } 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..5e19e248 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 @@ -5,12 +5,7 @@ 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.mockito.*; import java.io.IOException; import java.nio.file.Files; @@ -24,18 +19,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; 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.*; class HtmlUtilsTest { - private static MockedStatic pathMockedStatic; - private static MockedStatic filesMockedStatic; + private MockedStatic pathMockedStatic; + private MockedStatic filesMockedStatic; private final String testId = "testId"; + @Mock + private ContextManager contextManager; + + @Mock + private Map screenshots; + @Mock private Path path; @@ -62,6 +60,7 @@ void beforeEach() { pathMockedStatic = mockStatic(Path.class); filesMockedStatic = mockStatic(Files.class); + Reflections.setField("contextManager", htmlUtils, contextManager); Reflections.setField("freeMarkerWrapper", htmlUtils, freeMarkerWrapper); Reflections.setField("fileUtils", htmlUtils, fileUtils); } @@ -141,10 +140,10 @@ void buildFrameTagFor() { @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