diff --git a/CHANGELOG.md b/CHANGELOG.md index d7469f0..6816e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Changed +- Broader catch in screenshot and page source getting methods to avoid throwing any exceptions in logger, by @HardNorth +- Constant names refactoring, by @HardNorth ## [5.1.1] ### Changed diff --git a/README.md b/README.md index 7f58ce3..d49c896 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ The latest version: 5.1.1. Please use `Maven Central` link above to get the agen ## Overview -Selenide step logger for Report Portal - -Description TBD +Selenide step logging listener for Report Portal. The listener listen for Selenide log events and send them to Report Portal as steps. +It has ability to log screenshots and page sources on failure, this is enabled by default. Also, it is possible to attach different types of +WebDriver logs on failure. ## Configuration @@ -61,11 +61,49 @@ base class for all tests: ```java public class BaseTest { static { - SelenideLogger.addListener("ReportPortal logger", new ReportPortalSelenideEventListener()); + SelenideLogger.addListener("Report Portal logger", new ReportPortalSelenideEventListener()); } } ``` ### Logger configuration -TBD +#### Screenshots and page sources + +The logger has screenshots and page sources logging enabled by default. It logs them inside the step on every failure. To disable / enable +this behavior we have separate setters methods in the logger. +E.G.: +```java +public class BaseTest { + static { + SelenideLogger.addListener("Report Portal logger", + new ReportPortalSelenideEventListener().logScreenshots(false).logPageSources(false)); + } +} +``` +This disables both: screenshot and page sources logging. + +#### Selenium logs + +The logger can also attach Selenium logs on step failure. To enable it you need to call specific setter method inside the listener and +bypass desired log type and level: +```java +public class BaseTest { + static { + SelenideLogger.addListener("Report Portal logger", + new ReportPortalSelenideEventListener().enableSeleniumLogs(LogType.BROWSER, Level.FINER)); + } +} +``` + +#### Step name sanitizing + +If you need to hide some secret data from you step logs you can do this by specifying step name converter in logger constructor. +```java +public class BaseTest { + static { + SelenideLogger.addListener("Report Portal logger", + new ReportPortalSelenideEventListener(LogLevel.INFO, l -> l.replaceAll("secret_token=[^&]*", "secret_token="))); + } +} +``` diff --git a/README_TEMPLATE.md b/README_TEMPLATE.md index a3e0fb3..e6c1607 100644 --- a/README_TEMPLATE.md +++ b/README_TEMPLATE.md @@ -11,9 +11,9 @@ The latest version: $LATEST_VERSION. Please use `Maven Central` link above to ge ## Overview -Selenide step logger for Report Portal - -Description TBD +Selenide step logging listener for Report Portal. The listener listen for Selenide log events and send them to Report Portal as steps. +It has ability to log screenshots and page sources on failure, this is enabled by default. Also, it is possible to attach different types of +WebDriver logs on failure. ## Configuration @@ -61,11 +61,50 @@ base class for all tests: ```java public class BaseTest { static { - SelenideLogger.addListener("ReportPortal logger", new ReportPortalSelenideEventListener()); + SelenideLogger.addListener("Report Portal logger", new ReportPortalSelenideEventListener()); } } ``` ### Logger configuration -TBD +#### Screenshots and page sources + +The logger has screenshots and page sources logging enabled by default. It logs them inside the step on every failure. To disable / enable +this behavior we have separate setters methods in the logger. +E.G.: +```java +public class BaseTest { + static { + SelenideLogger.addListener("Report Portal logger", + new ReportPortalSelenideEventListener().logScreenshots(false).logPageSources(false)); + } +} +``` +This disables both: screenshot and page sources logging. + +#### Selenium logs + +The logger can also attach Selenium logs on step failure. To enable it you need to call specific setter method inside the listener and +bypass desired log type and level: +```java +public class BaseTest { + static { + SelenideLogger.addListener("Report Portal logger", + new ReportPortalSelenideEventListener().enableSeleniumLogs(LogType.BROWSER, Level.FINER)); + } +} +``` + +#### Step name sanitizing + +If you need to hide some secret data from you step logs you can do this by specifying step name converter in logger constructor. +```java +public class BaseTest { + static { + SelenideLogger.addListener("Report Portal logger", + new ReportPortalSelenideEventListener(LogLevel.INFO, l -> l.replaceAll("secret_token=[^&]*", "secret_token="))); + } +} +``` + diff --git a/src/main/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListener.java b/src/main/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListener.java index f7bce03..f1ef701 100644 --- a/src/main/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListener.java +++ b/src/main/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListener.java @@ -29,7 +29,6 @@ import com.google.common.io.ByteSource; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriverException; import javax.annotation.Nonnull; import java.nio.charset.StandardCharsets; @@ -39,10 +38,20 @@ import static java.util.Optional.ofNullable; +/** + * Selenide step logging listener for Report Portal. + *

+ * The listener listen for Selenide log events and send them to Report Portal as steps. It has ability to log screenshots and page sources + * on failure, this is enabled by default. Also, it is possible to attach different types of WebDriver logs on failure. + *

+ * Basic usage: + *

+ *     SelenideLogger.addListener("Report Portal logger", new ReportPortalSelenideEventListener());
+ * 
+ */ public class ReportPortalSelenideEventListener implements LogEventListener { - public static final String UNABLE_TO_PROCESS_SELENIDE_EVENT_STATUS = "Unable to process selenide event status, skipping it: "; - public static final Function DEFAULT_LOG_CONVERTER = log -> log; + public static final Function DEFAULT_STEP_NAME_CONVERTER = log -> log; private static final String SCREENSHOT_MESSAGE = "Screenshot"; private static final String PAGE_SOURCE_MESSAGE = "Page source"; @@ -60,44 +69,95 @@ public class ReportPortalSelenideEventListener implements LogEventListener { private boolean screenshots = true; private boolean pageSources = true; + /** + * Create listener instance with specified log level and step name converter. + * + * @param defaultLogLevel logging level of attachments + * @param stepConverter step name converter, suitable to sanitize step string from secret data + */ public ReportPortalSelenideEventListener(@Nonnull LogLevel defaultLogLevel, Function stepConverter) { logLevel = defaultLogLevel.name(); converter = stepConverter; } + /** + * Create listener instance with specified log level. + * + * @param defaultLogLevel logging level of attachments + */ public ReportPortalSelenideEventListener(@Nonnull LogLevel defaultLogLevel) { - this(defaultLogLevel, DEFAULT_LOG_CONVERTER); + this(defaultLogLevel, DEFAULT_STEP_NAME_CONVERTER); } + /** + * Create listener instance with default attachment {@link LogLevel}: "INFO". + */ public ReportPortalSelenideEventListener() { this(LogLevel.INFO); } + /** + * Set screenshot on failure logging enable/disable. Enabled by default. + * + * @param logScreenshots use false to disable screenshot logging + * @return self instance for convenience + */ public ReportPortalSelenideEventListener logScreenshots(boolean logScreenshots) { this.screenshots = logScreenshots; return this; } + /** + * Set page sources on failure logging enable/disable. Enabled by default. + * + * @param logPageSources use false to disable page sources logging + * @return self instance for convenience + */ public ReportPortalSelenideEventListener logPageSources(boolean logPageSources) { this.pageSources = logPageSources; return this; } + /** + * Enable certain selenium log attach on failure. + * + * @param logType a string from {@link org.openqa.selenium.logging.LogType} describing desired log type to be logged + * @param logLevel desired log level to see in attachment + * @return self instance for convenience + */ public ReportPortalSelenideEventListener enableSeleniumLogs(@Nonnull String logType, @Nonnull Level logLevel) { seleniumLogTypes.put(logType, logLevel); return this; } + /** + * Disable certain selenium log attach on failure. + * + * @param logType a string from {@link org.openqa.selenium.logging.LogType} describing desired log type to be muted + * @return self instance for convenience + */ public ReportPortalSelenideEventListener disableSeleniumLogs(@Nonnull String logType) { seleniumLogTypes.remove(logType); return this; } + /** + * Enable custom selenide step logging. + * + * @param selenideLogType type of selenide event to enable logging + * @return self instance for convenience + */ public ReportPortalSelenideEventListener enableSelenideLogs(@Nonnull Class selenideLogType) { selenideLogTypes.add(selenideLogType); return this; } + /** + * Disable custom selenide step logging. + * + * @param selenideLogType type of selenide event to mute + * @return self instance for convenience + */ public ReportPortalSelenideEventListener disableSelenideLogs(@Nonnull Class selenideLogType) { selenideLogTypes.remove(selenideLogType); return this; @@ -130,7 +190,7 @@ private void logScreenshot() { if ((screenshot = ((TakesScreenshot) WebDriverRunner.getWebDriver()).getScreenshotAs(OutputType.BYTES)) != null) { attachBinary(SCREENSHOT_MESSAGE, screenshot, SELENIUM_SCREENSHOT_TYPE); } - } catch (WebDriverException e) { + } catch (Exception e) { ReportPortal.emitLog("Unable to get WebDriver screenshot: " + e.getMessage(), LogLevel.ERROR.name(), Calendar.getInstance().getTime() @@ -146,7 +206,7 @@ private void logPageSource() { if ((pageSource = WebDriverRunner.getWebDriver().getPageSource()) != null) { attachBinary(PAGE_SOURCE_MESSAGE, pageSource.getBytes(StandardCharsets.UTF_8), SELENIUM_PAGE_SOURCE_TYPE); } - } catch (WebDriverException e) { + } catch (Exception e) { ReportPortal.emitLog("Unable to get WebDriver page source: " + e.getMessage(), LogLevel.ERROR.name(), Calendar.getInstance().getTime() @@ -179,7 +239,7 @@ public void afterEvent(@Nonnull LogEvent currentLog) { } else if (LogEvent.EventStatus.PASS.equals(currentLog.getStatus())) { ofNullable(Launch.currentLaunch()).ifPresent(l -> l.getStepReporter().finishPreviousStep()); } else { - ReportPortal.emitLog(UNABLE_TO_PROCESS_SELENIDE_EVENT_STATUS + currentLog.getStatus(), + ReportPortal.emitLog("Unable to process selenide event status, skipping it: " + currentLog.getStatus(), LogLevel.WARN.name(), Calendar.getInstance().getTime() ); diff --git a/src/test/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListenerTest.java b/src/test/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListenerTest.java index 930b034..2d918e1 100644 --- a/src/test/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListenerTest.java +++ b/src/test/java/com/epam/reportportal/selenide/ReportPortalSelenideEventListenerTest.java @@ -120,18 +120,17 @@ public void test_step_logging_failed() { when(webDriver.getScreenshotAs(eq(OutputType.BYTES))).thenReturn(image); when(webDriver.getPageSource()).thenReturn(page); + ReportPortalSelenideEventListener listener = new ReportPortalSelenideEventListener(); + runEvent(listener::beforeEvent, logEvent); + verify(context, noInteractions()).emit(any(Function.class)); + verify(stepReporter).sendStep(eq(ItemStatus.INFO), eq(SELENIDE_LOG_STRING)); + + when(logEvent.getStatus()).thenReturn(LogEvent.EventStatus.FAIL); + try (MockedStatic driverMockedStatic = Mockito.mockStatic(WebDriverRunner.class)) { driverMockedStatic.when(WebDriverRunner::hasWebDriverStarted).thenReturn(true); driverMockedStatic.when(WebDriverRunner::getWebDriver).thenReturn(webDriver); - ReportPortalSelenideEventListener listener = new ReportPortalSelenideEventListener(); - runEvent(listener::beforeEvent, logEvent); - verify(context, noInteractions()).emit(any(Function.class)); - - verify(stepReporter).sendStep(eq(ItemStatus.INFO), eq(SELENIDE_LOG_STRING)); - - when(logEvent.getStatus()).thenReturn(LogEvent.EventStatus.FAIL); - List> logs = runEventCapture(listener::afterEvent, logEvent); verify(stepReporter).finishPreviousStep(eq(ItemStatus.FAILED)); assertThat(logs, hasSize(2)); @@ -227,4 +226,64 @@ public void test_step_logging_failed_browser_logs() { } } } + + @Test + @SuppressWarnings({ "ResultOfMethodCallIgnored", "unchecked" }) + public void test_step_logging_failed_screenshot_exception() { + String exceptionMessage = "my exception message"; + LogEvent logEvent = mock(SelenideLog.class); + when(logEvent.toString()).thenReturn(SELENIDE_LOG_STRING); + ReportPortalSelenideEventListener listener = new ReportPortalSelenideEventListener().logPageSources(false); + runEvent(listener::beforeEvent, logEvent); + verify(context, noInteractions()).emit(any(Function.class)); + verify(stepReporter).sendStep(eq(ItemStatus.INFO), eq(SELENIDE_LOG_STRING)); + + when(logEvent.getStatus()).thenReturn(LogEvent.EventStatus.FAIL); + + RemoteWebDriver webDriver = mock(RemoteWebDriver.class); + when(webDriver.getScreenshotAs(any(OutputType.class))).thenThrow(new RuntimeException(exceptionMessage)); + try (MockedStatic driverMockedStatic = Mockito.mockStatic(WebDriverRunner.class)) { + driverMockedStatic.when(WebDriverRunner::hasWebDriverStarted).thenReturn(true); + driverMockedStatic.when(WebDriverRunner::getWebDriver).thenReturn(webDriver); + + List> logs = runEventCapture(listener::afterEvent, logEvent); + verify(stepReporter).finishPreviousStep(eq(ItemStatus.FAILED)); + assertThat(logs, hasSize(1)); + + SaveLogRQ screenshotLog = logs.get(0).apply("test"); + assertThat(screenshotLog.getLevel(), equalTo(LogLevel.ERROR.name())); + assertThat(screenshotLog.getFile(), nullValue()); + assertThat(screenshotLog.getMessage(), equalTo("Unable to get WebDriver screenshot: " + exceptionMessage)); + } + } + + @Test + @SuppressWarnings({ "ResultOfMethodCallIgnored", "unchecked" }) + public void test_step_logging_failed_page_source_exception() { + String exceptionMessage = "my exception message"; + LogEvent logEvent = mock(SelenideLog.class); + when(logEvent.toString()).thenReturn(SELENIDE_LOG_STRING); + ReportPortalSelenideEventListener listener = new ReportPortalSelenideEventListener().logScreenshots(false); + runEvent(listener::beforeEvent, logEvent); + verify(context, noInteractions()).emit(any(Function.class)); + verify(stepReporter).sendStep(eq(ItemStatus.INFO), eq(SELENIDE_LOG_STRING)); + + when(logEvent.getStatus()).thenReturn(LogEvent.EventStatus.FAIL); + + RemoteWebDriver webDriver = mock(RemoteWebDriver.class); + when(webDriver.getPageSource()).thenThrow(new RuntimeException(exceptionMessage)); + try (MockedStatic driverMockedStatic = Mockito.mockStatic(WebDriverRunner.class)) { + driverMockedStatic.when(WebDriverRunner::hasWebDriverStarted).thenReturn(true); + driverMockedStatic.when(WebDriverRunner::getWebDriver).thenReturn(webDriver); + + List> logs = runEventCapture(listener::afterEvent, logEvent); + verify(stepReporter).finishPreviousStep(eq(ItemStatus.FAILED)); + assertThat(logs, hasSize(1)); + + SaveLogRQ pageSourceLog = logs.get(0).apply("test"); + assertThat(pageSourceLog.getLevel(), equalTo(LogLevel.ERROR.name())); + assertThat(pageSourceLog.getFile(), nullValue()); + assertThat(pageSourceLog.getMessage(), equalTo("Unable to get WebDriver page source: " + exceptionMessage)); + } + } }