From e41e651767cc15dea9bca0842bf3d6721e86912d Mon Sep 17 00:00:00 2001 From: Sergey Dubovik <76820044+dubovik-sergey@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:50:42 +0300 Subject: [PATCH] [plugin-mobitru] Add ability to capture video recordings (#5307) Co-authored-by: Valery Yatsynovich --- .../modules/plugins/pages/plugin-mobitru.adoc | 6 + vividus-plugin-mobitru/build.gradle | 1 + .../vividus/mobitru/client/MobitruClient.java | 23 +++ .../vividus/mobitru/client/MobitruFacade.java | 17 ++ .../mobitru/client/MobitruFacadeImpl.java | 89 ++++++--- .../mobitru/client/ScreenRecording.java | 58 ++++++ .../exception/MobitruDeviceTakeException.java | 7 +- .../exception/MobitruOperationException.java | 7 +- .../client/model/ScreenRecordingMetadata.java | 21 +++ .../selenium/MobitruCapabilitiesAdjuster.java | 8 +- .../selenium/MobitruRecordingListener.java | 113 ++++++++++++ .../MobitruSessionInfoLocalStorage.java | 74 ++++++++ .../selenium/MobitruSessionInfoStorage.java | 26 +++ .../mobitru/mobile_app/profile.properties | 1 + .../main/resources/vividus-plugin/spring.xml | 10 ++ .../mobitru/client/MobitruClientTests.java | 70 +++++++- .../client/MobitruFacadeImplTests.java | 50 +++++- .../mobitru/client/ScreenRecordingTests.java | 49 +++++ .../MobitruCapabilitiesAdjusterTests.java | 5 + .../MobitruRecordingListenerTests.java | 170 ++++++++++++++++++ .../MobitruSessionInfoLocalStorageTests.java | 48 +++++ .../healthcheck/mobitru/suite.properties | 2 + 22 files changed, 819 insertions(+), 36 deletions(-) create mode 100644 vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/ScreenRecording.java create mode 100644 vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/model/ScreenRecordingMetadata.java create mode 100644 vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruRecordingListener.java create mode 100644 vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorage.java create mode 100644 vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoStorage.java create mode 100644 vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/ScreenRecordingTests.java create mode 100644 vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruRecordingListenerTests.java create mode 100644 vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorageTests.java diff --git a/docs/modules/plugins/pages/plugin-mobitru.adoc b/docs/modules/plugins/pages/plugin-mobitru.adoc index 5a8d90e101..93740ec433 100644 --- a/docs/modules/plugins/pages/plugin-mobitru.adoc +++ b/docs/modules/plugins/pages/plugin-mobitru.adoc @@ -127,6 +127,12 @@ a|`true` + |`false` |Inject special code into application to allow emulation of "touch id" action and QR code scan. +|`mobitru.video-recording-enabled` +a|`true` + +`false` +|`false` +|Enable the video recording for entire appium session. The output recording will be attached to the report. + |`mobitru.device-wait-timeout` |The duration in {durations-format-link} format. |`PT5M` diff --git a/vividus-plugin-mobitru/build.gradle b/vividus-plugin-mobitru/build.gradle index a62d47691a..761b2e9f42 100644 --- a/vividus-plugin-mobitru/build.gradle +++ b/vividus-plugin-mobitru/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation project(':vividus-http-client') implementation project(':vividus-extension-selenium') implementation project(':vividus-plugin-mobile-app') + implementation project(':vividus-reporter') implementation platform(group: 'org.slf4j', name: 'slf4j-bom', version: '2.0.16') implementation(group: 'org.slf4j', name: 'slf4j-api') diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java index 874494b030..1a6d0d6720 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruClient.java @@ -79,6 +79,29 @@ public void returnDevice(String deviceId) throws MobitruOperationException executeRequest(DEVICE_PATH + "/" + deviceId, HttpMethod.DELETE, UnaryOperator.identity(), HttpStatus.SC_OK); } + public void startDeviceScreenRecording(String deviceId) throws MobitruOperationException + { + performDeviceRecordingRequest(deviceId, HttpMethod.POST, HttpStatus.SC_CREATED); + } + + public byte[] stopDeviceScreenRecording(String deviceId) throws MobitruOperationException + { + return performDeviceRecordingRequest(deviceId, HttpMethod.DELETE, HttpStatus.SC_OK); + } + + private byte[] performDeviceRecordingRequest(String deviceId, HttpMethod httpMethod, int status) + throws MobitruOperationException + { + String url = String.format("%s/%s/recording", DEVICE_PATH, deviceId); + return executeRequest(url, httpMethod, UnaryOperator.identity(), status); + } + + public byte[] downloadDeviceScreenRecording(String recordingId) throws MobitruOperationException + { + return executeRequest("/recording/" + recordingId, HttpMethod.GET, + UnaryOperator.identity(), HttpStatus.SC_OK); + } + public void installApp(String deviceId, String appId, InstallApplicationOptions options) throws MobitruOperationException { diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacade.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacade.java index c52a37f310..9cfca58980 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacade.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacade.java @@ -31,6 +31,23 @@ public interface MobitruFacade */ String takeDevice(DesiredCapabilities desiredCapabilities) throws MobitruOperationException; + /** + * Start the screen recording on the device with specified UDID. + * + * @param deviceId The UDID of the device + * @throws MobitruOperationException In case of any issues during start the recording. + */ + void startDeviceScreenRecording(String deviceId) throws MobitruOperationException; + + /** + * Stop the screen recording on the device with specified UDID. + * + * @param deviceId The UDID of the device + * @return The ID of the recorded artifact. + * @throws MobitruOperationException In case of any issues during start the recording. + */ + ScreenRecording stopDeviceScreenRecording(String deviceId) throws MobitruOperationException; + /** * Installs the desired application on the device with specified UDID * diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java index 8890400c79..0daff1f50e 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/MobitruFacadeImpl.java @@ -21,8 +21,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -35,12 +36,14 @@ import org.openqa.selenium.remote.DesiredCapabilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import org.vividus.mobitru.client.exception.MobitruDeviceSearchException; import org.vividus.mobitru.client.exception.MobitruDeviceTakeException; import org.vividus.mobitru.client.exception.MobitruOperationException; import org.vividus.mobitru.client.model.Application; import org.vividus.mobitru.client.model.Device; import org.vividus.mobitru.client.model.DeviceSearchParameters; +import org.vividus.mobitru.client.model.ScreenRecordingMetadata; import org.vividus.util.wait.DurationBasedWaiter; import org.vividus.util.wait.RetryTimesBasedWaiter; import org.vividus.util.wait.WaitMode; @@ -54,9 +57,11 @@ public class MobitruFacadeImpl implements MobitruFacade private static final String APPIUM_UDID = "appium:udid"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); private static final int RETRY_TIMES = 20; + private static final Duration DOWNLOAD_RECORDING_POOLING_TIMEOUT = Duration.ofSeconds(5); + private static final int DOWNLOAD_RECORDING_RETRY_COUNT = 10; private final MobitruClient mobitruClient; private Duration waitForDeviceTimeout; @@ -83,8 +88,9 @@ public String takeDevice(DesiredCapabilities desiredCapabilities) throws Mobitru //use different API in case if udid is provided in capabilities //it's required in some cases like if the device is already taken LOGGER.info("Trying to take device with udid {}", deviceUdid); - return takeDevice(() -> mobitruClient.takeDeviceBySerial(String.valueOf(deviceUdid)), - () -> "Unable to take device with udid " + deviceUdid, getDefaultDeviceWaiter()); + return takeDevice(getDefaultDeviceWaiter(), + () -> mobitruClient.takeDeviceBySerial(String.valueOf(deviceUdid)), + () -> "Unable to take device with udid " + deviceUdid); } Device device = new Device(); device.setDesiredCapabilities(desiredCapabilities.asMap()); @@ -109,32 +115,41 @@ public void returnDevice(String deviceId) throws MobitruOperationException mobitruClient.returnDevice(deviceId); } + @Override + public void startDeviceScreenRecording(String deviceId) throws MobitruOperationException + { + mobitruClient.startDeviceScreenRecording(deviceId); + } + + @Override + public ScreenRecording stopDeviceScreenRecording(String deviceId) throws MobitruOperationException + { + byte[] receivedRecordingInfo = mobitruClient.stopDeviceScreenRecording(deviceId); + ScreenRecordingMetadata screenRecordingMetadataInfo = performMapperOperation(mapper -> + mapper.readValue(receivedRecordingInfo, ScreenRecordingMetadata.class)); + String recordingId = screenRecordingMetadataInfo.recordingId(); + Waiter waiter = new RetryTimesBasedWaiter(DOWNLOAD_RECORDING_POOLING_TIMEOUT, + DOWNLOAD_RECORDING_RETRY_COUNT); + byte[] recordingContent = performWaiterOperation(waiter, + () -> mobitruClient.downloadDeviceScreenRecording(recordingId), Level.DEBUG, + "download device screen recording", + e -> new MobitruOperationException("Unable to download recording with id " + recordingId, e)); + return new ScreenRecording(recordingId, recordingContent); + } + private String takeDevice(Device device, Waiter deviceWaiter) throws MobitruOperationException { LOGGER.info("Trying to take device with configuration {}", device); String capabilities = performMapperOperation(mapper -> mapper.writeValueAsString(device)); - return takeDevice(() -> mobitruClient.takeDevice(capabilities), - () -> "Unable to take device with configuration " + capabilities, deviceWaiter); + return takeDevice(deviceWaiter, () -> mobitruClient.takeDevice(capabilities), + () -> "Unable to take device with configuration " + capabilities); } - private String takeDevice(FailableSupplier takeDeviceActions, - Supplier unableToTakeDeviceErrorMessage, Waiter deviceWaiter) throws MobitruOperationException + private String takeDevice(Waiter waiter, FailableSupplier takeDeviceOperation, + Supplier unableToTakeDeviceErrorMessage) throws MobitruOperationException { - byte[] receivedDevice = deviceWaiter.wait(() -> { - try - { - return takeDeviceActions.get(); - } - catch (MobitruDeviceTakeException e) - { - LOGGER.warn("Unable to take device, retrying attempt.", e); - return null; - } - }, Objects::nonNull); - if (null == receivedDevice) - { - throw new MobitruDeviceTakeException(unableToTakeDeviceErrorMessage.get()); - } + byte[] receivedDevice = performWaiterOperation(waiter, takeDeviceOperation, Level.WARN, "take device", + e -> new MobitruDeviceTakeException(unableToTakeDeviceErrorMessage.get(), e)); Device takenDevice = performMapperOperation(mapper -> mapper.readValue(receivedDevice, Device.class)); LOGGER.info("Device with configuration {} is taken", takenDevice); return (String) takenDevice.getDesiredCapabilities().get(UDID); @@ -150,7 +165,7 @@ private String takeDevice(List devices) throws MobitruOperationException { return takeDevice(devicesIterator.next(), waiter); } - catch (MobitruDeviceTakeException e) + catch (MobitruOperationException e) { if (!devicesIterator.hasNext()) { @@ -193,10 +208,10 @@ private String findApp(String appRealName) throws MobitruOperationException { byte[] appsResponse = mobitruClient.getArtifacts(); List applications = performMapperOperation(mapper -> mapper.readerForListOf(Application.class) - .readValue(appsResponse)); + .readValue(appsResponse)); return applications.stream().filter(a -> appRealName.equals(a.getRealName())).findFirst().orElseThrow( () -> new MobitruOperationException(String.format("Unable to find application with the name `%s`." - + " The available applications are: %s", + + " The available applications are: %s", appRealName, applications))).getId(); } @@ -213,6 +228,26 @@ private R performMapperOperation(FailableFunction operation, + Level logLevel, String operationTitle, + Function exceptionOnMissingResult) + throws MobitruOperationException + { + AtomicReference lastException = new AtomicReference<>(null); + return waiter., RuntimeException>wait(() -> { + try + { + return Optional.of(operation.get()); + } + catch (MobitruOperationException e) + { + LOGGER.atLevel(logLevel).setCause(e).log("Unable to {}, retrying attempt", operationTitle); + lastException.set(e); + return Optional.empty(); + } + }, Optional::isPresent).orElseThrow(() -> exceptionOnMissingResult.apply(lastException.get())); + } + private boolean isSearchForDevice(DesiredCapabilities desiredCapabilities) { Map capabilities = desiredCapabilities.asMap(); @@ -224,7 +259,7 @@ private boolean isSearchForDevice(DesiredCapabilities desiredCapabilities) .filter(capabilities::containsKey).findFirst(); Validate.isTrue(conflictingCapability.isEmpty(), "Conflicting capabilities are found. `%s` capability can not be specified along with " - + "`mobitru-device-search:` capabilities", + + "`mobitru-device-search:` capabilities", conflictingCapability.orElse(null)); } return containsSearchCapabilities; diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/ScreenRecording.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/ScreenRecording.java new file mode 100644 index 0000000000..29dda7f264 --- /dev/null +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/ScreenRecording.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.client; + +import java.util.Arrays; +import java.util.Objects; + +public record ScreenRecording(String recordingId, byte[] content) +{ + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof ScreenRecording that)) + { + return false; + } + return Objects.equals(recordingId, that.recordingId) && Arrays.equals(content, that.content); + } + + @Override + public int hashCode() + { + return Objects.hash(recordingId, Arrays.hashCode(content)); + } + + @Override + public String toString() + { + StringBuilder result = new StringBuilder("ScreenRecording{recordingId=").append(recordingId).append(", "); + if (content == null) + { + result.append("content=null"); + } + else + { + result.append("contentLength=").append(content.length).append(" bytes"); + } + return result.append('}').toString(); + } +} diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruDeviceTakeException.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruDeviceTakeException.java index 4684b7bfe8..d49218d938 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruDeviceTakeException.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruDeviceTakeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,4 +24,9 @@ public MobitruDeviceTakeException(String message) { super(message); } + + public MobitruDeviceTakeException(String message, Throwable cause) + { + super(message, cause); + } } diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruOperationException.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruOperationException.java index ed01486f9f..68b6638a9b 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruOperationException.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/exception/MobitruOperationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,4 +29,9 @@ public MobitruOperationException(Throwable cause) { super(cause); } + + public MobitruOperationException(String message, Throwable cause) + { + super(message, cause); + } } diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/model/ScreenRecordingMetadata.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/model/ScreenRecordingMetadata.java new file mode 100644 index 0000000000..69112a8699 --- /dev/null +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/client/model/ScreenRecordingMetadata.java @@ -0,0 +1,21 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.client.model; + +public record ScreenRecordingMetadata(String recordingId) +{ +} diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjuster.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjuster.java index 167886b631..5be07f7777 100644 --- a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjuster.java +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjuster.java @@ -27,15 +27,18 @@ public class MobitruCapabilitiesAdjuster extends DesiredCapabilitiesAdjuster { private static final String APPIUM_UDID = "appium:udid"; - private final MobitruFacade mobitruFacade; private final InstallApplicationOptions installApplicationOptions; + private final MobitruFacade mobitruFacade; + private final MobitruSessionInfoStorage mobitruSessionInfoStorage; private String appFileName; - public MobitruCapabilitiesAdjuster(InstallApplicationOptions installApplicationOptions, MobitruFacade mobitruFacade) + public MobitruCapabilitiesAdjuster(InstallApplicationOptions installApplicationOptions, MobitruFacade mobitruFacade, + MobitruSessionInfoStorage mobitruSessionInfoStorage) { this.installApplicationOptions = installApplicationOptions; this.mobitruFacade = mobitruFacade; + this.mobitruSessionInfoStorage = mobitruSessionInfoStorage; } @Override @@ -46,6 +49,7 @@ public Map getExtraCapabilities(DesiredCapabilities desiredCapab { deviceId = mobitruFacade.takeDevice(desiredCapabilities); mobitruFacade.installApp(deviceId, appFileName, installApplicationOptions); + mobitruSessionInfoStorage.saveDeviceId(deviceId); Map capabilities = desiredCapabilities.asMap(); if (capabilities.containsKey(APPIUM_UDID) || capabilities.containsKey("udid")) { diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruRecordingListener.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruRecordingListener.java new file mode 100644 index 0000000000..539113cadb --- /dev/null +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruRecordingListener.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.selenium; + +import com.google.common.eventbus.Subscribe; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.jbehave.core.annotations.AfterScenario; +import org.jbehave.core.annotations.BeforeScenario; +import org.jbehave.core.annotations.ScenarioType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vividus.mobitru.client.MobitruFacade; +import org.vividus.mobitru.client.ScreenRecording; +import org.vividus.mobitru.client.exception.MobitruOperationException; +import org.vividus.reporter.event.IAttachmentPublisher; +import org.vividus.selenium.event.BeforeWebDriverQuitEvent; +import org.vividus.selenium.event.WebDriverCreateEvent; + +public class MobitruRecordingListener +{ + private static final Logger LOGGER = LoggerFactory.getLogger(MobitruRecordingListener.class); + + private final boolean videoRecordingEnabled; + private final MobitruFacade mobitruFacade; + private final MobitruSessionInfoStorage mobitruSessionInfoStorage; + private final IAttachmentPublisher attachmentPublisher; + + public MobitruRecordingListener(boolean videoRecordingEnabled, MobitruFacade mobitruFacade, + MobitruSessionInfoStorage mobitruSessionInfoStorage, IAttachmentPublisher attachmentPublisher) + { + this.videoRecordingEnabled = videoRecordingEnabled; + this.mobitruFacade = mobitruFacade; + this.attachmentPublisher = attachmentPublisher; + this.mobitruSessionInfoStorage = mobitruSessionInfoStorage; + } + + @BeforeScenario(uponType = ScenarioType.ANY) + public void startRecordingBeforeScenario() + { + // It is possible to create several video recordings within a single session + startRecordingIfEnabled(); + } + + @Subscribe + public void onSessionStart(WebDriverCreateEvent event) + { + startRecordingIfEnabled(); + } + + @Subscribe + public void onBeforeSessionStop(BeforeWebDriverQuitEvent event) + { + publishRecordingIfEnabled(); + } + + @AfterScenario(uponType = ScenarioType.ANY) + public void publishRecordingAfterScenario() + { + publishRecordingIfEnabled(); + } + + private void startRecordingIfEnabled() + { + performRecordingOperation(mobitruFacade::startDeviceScreenRecording, + "Unable to start recording on the device with UUID {}"); + } + + private void publishRecordingIfEnabled() + { + performRecordingOperation(deviceId -> { + ScreenRecording recording = mobitruFacade.stopDeviceScreenRecording(deviceId); + String attachmentName = String.format("Video Recording (%s).mp4", recording.recordingId()); + attachmentPublisher.publishAttachment(recording.content(), attachmentName); + }, "Unable to retrieve recording from the device with UUID {}"); + } + + private void performRecordingOperation(FailableConsumer deviceIdConsumer, + String logMessageTemplate) + { + if (videoRecordingEnabled) + { + mobitruSessionInfoStorage.getDeviceId().ifPresent(deviceId -> + { + try + { + deviceIdConsumer.accept(deviceId); + } + catch (MobitruOperationException e) + { + LOGGER.atWarn() + .addArgument(deviceId) + .setCause(e) + .log(logMessageTemplate); + } + }); + } + } +} diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorage.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorage.java new file mode 100644 index 0000000000..bffe48254c --- /dev/null +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorage.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.selenium; + +import java.util.Optional; + +import com.google.common.eventbus.Subscribe; + +import org.vividus.selenium.event.AfterWebDriverQuitEvent; +import org.vividus.testcontext.TestContext; + +public class MobitruSessionInfoLocalStorage implements MobitruSessionInfoStorage +{ + private static final Object KEY = MobitruSessionInfo.class; + + private final TestContext testContext; + + public MobitruSessionInfoLocalStorage(TestContext testContext) + { + this.testContext = testContext; + } + + @Override + public Optional getDeviceId() + { + return Optional.ofNullable(getMobitruSessionInfo().getDeviceId()); + } + + @Override + public void saveDeviceId(String deviceId) + { + getMobitruSessionInfo().setDeviceId(deviceId); + } + + @Subscribe + public void onAfterSessionStop(AfterWebDriverQuitEvent event) + { + testContext.remove(KEY); + } + + private MobitruSessionInfo getMobitruSessionInfo() + { + return testContext.get(KEY, MobitruSessionInfo::new); + } + + private static final class MobitruSessionInfo + { + private String deviceId; + + private String getDeviceId() + { + return deviceId; + } + + private void setDeviceId(String deviceId) + { + this.deviceId = deviceId; + } + } +} diff --git a/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoStorage.java b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoStorage.java new file mode 100644 index 0000000000..162d2c0482 --- /dev/null +++ b/vividus-plugin-mobitru/src/main/java/org/vividus/mobitru/selenium/MobitruSessionInfoStorage.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.selenium; + +import java.util.Optional; + +public interface MobitruSessionInfoStorage +{ + Optional getDeviceId(); + + void saveDeviceId(String deviceId); +} diff --git a/vividus-plugin-mobitru/src/main/resources/properties/profile/mobitru/mobile_app/profile.properties b/vividus-plugin-mobitru/src/main/resources/properties/profile/mobitru/mobile_app/profile.properties index d5f6bfcbbd..f2936e4ca4 100644 --- a/vividus-plugin-mobitru/src/main/resources/properties/profile/mobitru/mobile_app/profile.properties +++ b/vividus-plugin-mobitru/src/main/resources/properties/profile/mobitru/mobile_app/profile.properties @@ -5,3 +5,4 @@ selenium.grid.capabilities.automationName=${selenium.grid.automation-name} mobitru.device-wait-timeout=PT5M mobitru.resign-ios-app=true mobitru.do-injection=false +mobitru.video-recording-enabled=false diff --git a/vividus-plugin-mobitru/src/main/resources/vividus-plugin/spring.xml b/vividus-plugin-mobitru/src/main/resources/vividus-plugin/spring.xml index e48a8f9867..cebc6285a1 100644 --- a/vividus-plugin-mobitru/src/main/resources/vividus-plugin/spring.xml +++ b/vividus-plugin-mobitru/src/main/resources/vividus-plugin/spring.xml @@ -21,6 +21,12 @@ + + + + + + @@ -44,4 +50,8 @@ + + + + diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java index ef3bb9e2b4..83d7d7a608 100644 --- a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruClientTests.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import java.io.IOException; @@ -55,6 +56,12 @@ class MobitruClientTests private static final String DEVICE_ID = "deviceid"; private static final String TAKE_DEVICE_ENDPOINT = "/billing/unit/vividus/automation/api/device"; private static final String UDID = "Z3CT103D2DZ"; + private static final String DELIMITER = "/"; + private static final String DEVICE_RECORDING_ENDPOINT = TAKE_DEVICE_ENDPOINT. + concat(DELIMITER).concat(UDID). + concat(DELIMITER).concat("recording"); + private static final String RECORDING_ENDPOINT = "/billing/unit/vividus/automation/api/recording"; + private static final String RECORDING_ID = "5eefb29c-78a4-4f39-ac52-bd54a22c5243"; @Mock private IHttpClient httpClient; @Mock private HttpResponse httpResponse; @@ -110,6 +117,67 @@ void shouldTakeDevice() throws IOException, MobitruOperationException } } + @Test + void shouldStartRecording() throws IOException, MobitruOperationException + { + var builder = mock(HttpRequestBuilder.class); + ClassicHttpRequest httpRequest = mock(); + try (MockedStatic builderMock = Mockito.mockStatic(HttpRequestBuilder.class)) + { + builderMock.when(HttpRequestBuilder::create).thenReturn(builder); + when(builder.withEndpoint(ENDPOINT)).thenReturn(builder); + when(builder.withHttpMethod(HttpMethod.POST)).thenReturn(builder); + when(builder.withRelativeUrl(DEVICE_RECORDING_ENDPOINT)).thenReturn(builder); + when(builder.build()).thenReturn(httpRequest); + when(httpClient.execute(httpRequest)).thenReturn(httpResponse); + when(httpResponse.getStatusCode()).thenReturn(HttpStatus.SC_CREATED); + mobitruClient.startDeviceScreenRecording(UDID); + verify(httpResponse).getResponseBody(); + verifyNoMoreInteractions(httpClient, httpResponse); + } + } + + @Test + void shouldStopRecording() throws IOException, MobitruOperationException + { + var builder = mock(HttpRequestBuilder.class); + ClassicHttpRequest httpRequest = mock(); + try (MockedStatic builderMock = Mockito.mockStatic(HttpRequestBuilder.class)) + { + builderMock.when(HttpRequestBuilder::create).thenReturn(builder); + builderMock.when(HttpRequestBuilder::create).thenReturn(builder); + when(builder.withEndpoint(ENDPOINT)).thenReturn(builder); + when(builder.withHttpMethod(HttpMethod.DELETE)).thenReturn(builder); + when(builder.withRelativeUrl(DEVICE_RECORDING_ENDPOINT)).thenReturn(builder); + when(builder.build()).thenReturn(httpRequest); + when(httpClient.execute(httpRequest)).thenReturn(httpResponse); + when(httpResponse.getResponseBody()).thenReturn(RESPONSE); + when(httpResponse.getStatusCode()).thenReturn(HttpStatus.SC_OK); + assertArrayEquals(RESPONSE, mobitruClient.stopDeviceScreenRecording(UDID)); + } + } + + @Test + void shouldDownloadRecording() throws IOException, MobitruOperationException + { + var builder = mock(HttpRequestBuilder.class); + ClassicHttpRequest httpRequest = mock(); + try (MockedStatic builderMock = Mockito.mockStatic(HttpRequestBuilder.class)) + { + builderMock.when(HttpRequestBuilder::create).thenReturn(builder); + builderMock.when(HttpRequestBuilder::create).thenReturn(builder); + when(builder.withEndpoint(ENDPOINT)).thenReturn(builder); + when(builder.withHttpMethod(HttpMethod.GET)).thenReturn(builder); + when(builder.withRelativeUrl(RECORDING_ENDPOINT.concat(DELIMITER).concat(RECORDING_ID))) + .thenReturn(builder); + when(builder.build()).thenReturn(httpRequest); + when(httpClient.execute(httpRequest)).thenReturn(httpResponse); + when(httpResponse.getResponseBody()).thenReturn(RESPONSE); + when(httpResponse.getStatusCode()).thenReturn(HttpStatus.SC_OK); + assertArrayEquals(RESPONSE, mobitruClient.downloadDeviceScreenRecording(RECORDING_ID)); + } + } + @Test void shouldTakeDeviceBySerial() throws IOException, MobitruOperationException { @@ -120,7 +188,7 @@ void shouldTakeDeviceBySerial() throws IOException, MobitruOperationException builderMock.when(HttpRequestBuilder::create).thenReturn(builder); when(builder.withEndpoint(ENDPOINT)).thenReturn(builder); when(builder.withHttpMethod(HttpMethod.POST)).thenReturn(builder); - when(builder.withRelativeUrl(TAKE_DEVICE_ENDPOINT.concat("/").concat(UDID))).thenReturn(builder); + when(builder.withRelativeUrl(TAKE_DEVICE_ENDPOINT.concat(DELIMITER).concat(UDID))).thenReturn(builder); when(builder.build()).thenReturn(httpRequest); when(httpClient.execute(httpRequest)).thenReturn(httpResponse); when(httpResponse.getResponseBody()).thenReturn(RESPONSE); diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java index de16cb9f77..9552625a10 100644 --- a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/MobitruFacadeImplTests.java @@ -16,6 +16,7 @@ package org.vividus.mobitru.client; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -57,14 +58,18 @@ @ExtendWith({ TestLoggerFactoryExtension.class, MockitoExtension.class }) class MobitruFacadeImplTests { + private static final byte[] BINARY_RESPONSE = {1, 0, 1, 0}; private static final String DEVICE_TAKEN_MESSAGE = "Device with configuration {} is taken"; private static final String TRYING_TO_TAKE_DEVICE_MESSAGE = "Trying to take device with configuration {}"; private static final String TRYING_TO_TAKE_DEVICE_UDID_MESSAGE = "Trying to take device with udid {}"; - private static final String RETRY_TO_TAKE_DEVICE_MESSAGE = "Unable to take device, retrying attempt."; + private static final String RETRY_LOG_MESSAGE = "Unable to {}, retrying attempt"; + private static final String TAKE_DEVICE_LOG_MESSAGE = "take device"; + private static final String DOWNLOAD_RECORDING_LOG_MESSAGE = "download device screen recording"; private static final String UNABLE_TO_TAKE_DEVICE_WITH_CONFIGURATION_ERROR_FORMAT = "Unable to take device with configuration %s"; private static final String UNABLE_TO_TAKE_DEVICE_WITH_UDID_ERROR_FORMAT = "Unable to take device with udid %s"; private static final String UDID = "Z3CT103D2DZ"; + private static final String RECORDING_ID = "15a02180-16f6-4eb9-b5e8-9e945f5e85fe"; private static final String DEVICE_TYPE_CAPABILITY_NAME = "mobitru-device-search:type"; private static final String PHONE = "phone"; private static final String PLATFORM_NAME = "platformName"; @@ -72,7 +77,11 @@ class MobitruFacadeImplTests private static final String IOS = "ios"; private static final String ANDROID = "android"; private static final String NO_DEVICE = "no device"; + private static final String NO_RECORDING = "no recording"; private static final String CAPABILITIES_JSON_PREFIX = "{\"desiredCapabilities\":{\"platformName\":\"Android\","; + private static final String STOP_RECORDING_JSON = "{\"status\": \"recording has stopped\"," + + + "\"recordingId\": \"15a02180-16f6-4eb9-b5e8-9e945f5e85fe\"}"; private static final String CAPABILITIES_IOS_PLATFORM = "{\"desiredCapabilities\":{\"platformName\":\"IOS\"}}"; private static final String CAPABILITIES_WITHOUT_UDID_JSON = CAPABILITIES_JSON_PREFIX + "\"platformVersion\":\"12\",\"deviceName\":\"SAMSUNG SM-G998B\"}}"; @@ -114,7 +123,7 @@ void shouldFindDeviceThenTakeItAndProvideItsUdid() throws MobitruOperationExcept LoggingEvent.info("Found devices: {}", System.lineSeparator() + "Device 1: " + failedTakeDevice + System.lineSeparator() + "Device 2: " + takenDevice), LoggingEvent.info(TRYING_TO_TAKE_DEVICE_MESSAGE, failedTakeDevice), - LoggingEvent.warn(deviceTakeException, RETRY_TO_TAKE_DEVICE_MESSAGE), + LoggingEvent.warn(deviceTakeException, RETRY_LOG_MESSAGE, TAKE_DEVICE_LOG_MESSAGE), LoggingEvent.warn(UNABLE_TO_TAKE_DEVICE_WITH_CONFIGURATION_ERROR_FORMAT.formatted( OBJECT_MAPPER.writeValueAsString(failedTakeDevice))), LoggingEvent.info(TRYING_TO_TAKE_DEVICE_MESSAGE, takenDevice), @@ -134,7 +143,7 @@ void shouldTakeDeviceByUdidAndProvideItsUdid(String capName) throws MobitruOpera var takenDevice = getTestDevice(); assertEquals(List.of( LoggingEvent.info(TRYING_TO_TAKE_DEVICE_UDID_MESSAGE, UDID), - LoggingEvent.warn(deviceTakeException, RETRY_TO_TAKE_DEVICE_MESSAGE), + LoggingEvent.warn(deviceTakeException, RETRY_LOG_MESSAGE, TAKE_DEVICE_LOG_MESSAGE), LoggingEvent.info(DEVICE_TAKEN_MESSAGE, takenDevice)), LOGGER.getLoggingEvents()); } @@ -152,10 +161,43 @@ void shouldTakeDeviceInUseAndProvideItsUdId() throws MobitruOperationException assertEquals(UDID, mobitruFacadeImpl.takeDevice(desiredCapabilities)); assertEquals(List.of(LoggingEvent.info(TRYING_TO_TAKE_DEVICE_MESSAGE, requestedDevice), - LoggingEvent.warn(exception, RETRY_TO_TAKE_DEVICE_MESSAGE), + LoggingEvent.warn(exception, RETRY_LOG_MESSAGE, TAKE_DEVICE_LOG_MESSAGE), LoggingEvent.info(DEVICE_TAKEN_MESSAGE, getTestDevice())), LOGGER.getLoggingEvents()); } + @Test + void shouldStartDeviceSessionRecording() + { + assertDoesNotThrow(() -> mobitruFacadeImpl.startDeviceScreenRecording(UDID)); + } + + @Test + void shouldDownloadDeviceSessionRecording() throws MobitruOperationException + { + var noRecordingException = new MobitruDeviceTakeException(NO_RECORDING); + when(mobitruClient.stopDeviceScreenRecording(UDID)). + thenReturn(STOP_RECORDING_JSON.getBytes(StandardCharsets.UTF_8)); + when(mobitruClient.downloadDeviceScreenRecording(RECORDING_ID)).thenThrow(noRecordingException). + thenReturn(BINARY_RESPONSE); + assertEquals(new ScreenRecording(RECORDING_ID, BINARY_RESPONSE), + mobitruFacadeImpl.stopDeviceScreenRecording(UDID)); + assertEquals(List.of(LoggingEvent.debug(noRecordingException, RETRY_LOG_MESSAGE, + DOWNLOAD_RECORDING_LOG_MESSAGE)), LOGGER.getLoggingEvents()); + } + + @Test + void shouldThrowExceptionIfRecordingIsNotDownloaded() throws MobitruOperationException + { + var noRecordingException = new MobitruDeviceTakeException(NO_RECORDING); + when(mobitruClient.stopDeviceScreenRecording(UDID)). + thenReturn(STOP_RECORDING_JSON.getBytes(StandardCharsets.UTF_8)); + when(mobitruClient.downloadDeviceScreenRecording(RECORDING_ID)).thenThrow(noRecordingException); + var exception = assertThrows(MobitruOperationException.class, + () -> mobitruFacadeImpl.stopDeviceScreenRecording(UDID)); + assertEquals(String.format("Unable to download recording with id %s", RECORDING_ID), + exception.getMessage()); + } + @ParameterizedTest @ValueSource(strings = { "appium:udid", UDID_CAP, "deviceName", "appium:deviceName" }) void shouldThrowExceptionIfConflictingCapabilities(String capabilityName) diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/ScreenRecordingTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/ScreenRecordingTests.java new file mode 100644 index 0000000000..4e5c4822c2 --- /dev/null +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/client/ScreenRecordingTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; + +class ScreenRecordingTests +{ + @Test + void validateHashCodeAndEquals() + { + EqualsVerifier.simple().forClass(ScreenRecording.class) + .suppress(Warning.NULL_FIELDS) + .verify(); + } + + @Test + void testToStringWithNullContent() + { + var screenRecording = new ScreenRecording("id", null); + assertEquals("ScreenRecording{recordingId=id, content=null}", screenRecording.toString()); + } + + @Test + void testToStringWithNonNullContent() + { + var screenRecording = new ScreenRecording(null, new byte[] { 1, 0, 1 }); + assertEquals("ScreenRecording{recordingId=null, contentLength=3 bytes}", screenRecording.toString()); + } +} diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjusterTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjusterTests.java index 58c92ecc98..a6ddcf47c3 100644 --- a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjusterTests.java +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruCapabilitiesAdjusterTests.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.util.Map; @@ -49,6 +50,7 @@ class MobitruCapabilitiesAdjusterTests @Mock private MobitruFacade mobitruFacade; @Mock private InstallApplicationOptions installApplicationOptions; + @Mock private MobitruSessionInfoStorage mobitruSessionInfoStorage; @InjectMocks private MobitruCapabilitiesAdjuster mobitruCapabilitiesConfigurer; @Test @@ -59,6 +61,7 @@ void shouldTakeDeviceAndInstallAnApp() throws MobitruOperationException when(mobitruFacade.takeDevice(capabilities)).thenReturn(UDID); var ordered = Mockito.inOrder(mobitruFacade); assertEquals(Map.of(APPIUM_UDID, UDID), mobitruCapabilitiesConfigurer.getExtraCapabilities(capabilities)); + verify(mobitruSessionInfoStorage).saveDeviceId(UDID); ordered.verify(mobitruFacade).takeDevice(capabilities); ordered.verify(mobitruFacade).installApp(UDID, STEAM_APK, installApplicationOptions); verify(mobitruFacade, never()).returnDevice(UDID); @@ -74,6 +77,7 @@ void shouldNotReturnUdidIfItsAlreadyPresentedInCapabilities(String udidCapabilit when(mobitruFacade.takeDevice(capabilities)).thenReturn(UDID); var ordered = Mockito.inOrder(mobitruFacade); assertEquals(Map.of(), mobitruCapabilitiesConfigurer.getExtraCapabilities(capabilities)); + verify(mobitruSessionInfoStorage).saveDeviceId(UDID); ordered.verify(mobitruFacade).takeDevice(capabilities); ordered.verify(mobitruFacade).installApp(UDID, STEAM_APK, installApplicationOptions); verify(mobitruFacade, never()).returnDevice(UDID); @@ -88,6 +92,7 @@ void shouldWrapExceptionAndStopDeviceUsage() throws MobitruOperationException when(mobitruFacade.takeDevice(capabilities)).thenReturn(UDID); doThrow(exception).when(mobitruFacade).installApp(UDID, STEAM_APK, installApplicationOptions); var iae = assertThrows(IllegalStateException.class, () -> mobitruCapabilitiesConfigurer.adjust(capabilities)); + verifyNoInteractions(mobitruSessionInfoStorage); verify(mobitruFacade).returnDevice(UDID); assertEquals(exception, iae.getCause()); } diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruRecordingListenerTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruRecordingListenerTests.java new file mode 100644 index 0000000000..99562f57a2 --- /dev/null +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruRecordingListenerTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.selenium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import com.github.valfirst.slf4jtest.LoggingEvent; +import com.github.valfirst.slf4jtest.TestLogger; +import com.github.valfirst.slf4jtest.TestLoggerFactory; +import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.vividus.mobitru.client.MobitruFacade; +import org.vividus.mobitru.client.ScreenRecording; +import org.vividus.mobitru.client.exception.MobitruOperationException; +import org.vividus.reporter.event.IAttachmentPublisher; +import org.vividus.selenium.event.BeforeWebDriverQuitEvent; +import org.vividus.selenium.event.WebDriverCreateEvent; + +@ExtendWith({ TestLoggerFactoryExtension.class, MockitoExtension.class }) +class MobitruRecordingListenerTests +{ + private static final byte[] BINARY_RESPONSE = {1, 0, 1, 0}; + private static final String RECORDING_ID = "15a02180-16f6-4eb9-b5e8-9e945f5e85fe"; + private static final String RECORDING_REPORT_NAME = String.format("Video Recording (%s).mp4", + RECORDING_ID); + private static final String UDID = "Z3CV103D2DO"; + + private final TestLogger logger = TestLoggerFactory.getTestLogger(MobitruRecordingListener.class); + + @Mock private MobitruFacade mobitruFacade; + @Mock private MobitruSessionInfoStorage mobitruSessionInfoStorage; + @Mock private IAttachmentPublisher attachmentPublisher; + + @Test + void shouldStartRecording() throws MobitruOperationException + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.of(UDID)); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.onSessionStart(mock(WebDriverCreateEvent.class)); + verify(mobitruFacade).startDeviceScreenRecording(UDID); + } + + @Test + void shouldNotStartRecordingIfDisabled() + { + var mobitruRecordingListener = new MobitruRecordingListener(false, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.onSessionStart(mock(WebDriverCreateEvent.class)); + verifyNoInteractions(mobitruSessionInfoStorage, mobitruFacade); + } + + @Test + void shouldStartRecordingBeforeScenario() throws MobitruOperationException + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.of(UDID)); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.startRecordingBeforeScenario(); + verify(mobitruFacade).startDeviceScreenRecording(UDID); + } + + @Test + void shouldNotStartRecordingIfDriverIsNotInitialized() + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.empty()); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.startRecordingBeforeScenario(); + verifyNoInteractions(mobitruFacade); + } + + @Test + void shouldAttachRecordingOnSessionFinish() throws MobitruOperationException + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.of(UDID)); + var recording = new ScreenRecording(RECORDING_ID, BINARY_RESPONSE); + when(mobitruFacade.stopDeviceScreenRecording(UDID)).thenReturn(recording); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.onBeforeSessionStop(mock(BeforeWebDriverQuitEvent.class)); + verify(attachmentPublisher).publishAttachment(BINARY_RESPONSE, RECORDING_REPORT_NAME); + } + + @Test + void shouldNotStopRecordingIfDisabled() + { + var mobitruRecordingListener = new MobitruRecordingListener(false, mobitruFacade, + mobitruSessionInfoStorage, attachmentPublisher); + mobitruRecordingListener.onBeforeSessionStop(mock(BeforeWebDriverQuitEvent.class)); + verifyNoInteractions(mobitruSessionInfoStorage, mobitruFacade, attachmentPublisher); + } + + @Test + void shouldPublishRecordingAfterScenario() throws MobitruOperationException + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.of(UDID)); + var recording = new ScreenRecording(RECORDING_ID, BINARY_RESPONSE); + when(mobitruFacade.stopDeviceScreenRecording(UDID)).thenReturn(recording); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.publishRecordingAfterScenario(); + verify(attachmentPublisher).publishAttachment(BINARY_RESPONSE, RECORDING_REPORT_NAME); + } + + @Test + void shouldNotPublishRecordingIfDriverIsNotInitialized() + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.empty()); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.publishRecordingAfterScenario(); + verifyNoInteractions(mobitruFacade, attachmentPublisher); + } + + @Test + void shouldLogWarnOnStartWithoutThrow() throws MobitruOperationException + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.of(UDID)); + var exception = new MobitruOperationException(UDID); + doThrow(exception).when(mobitruFacade).startDeviceScreenRecording(UDID); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.onSessionStart(mock(WebDriverCreateEvent.class)); + assertEquals(List.of( + LoggingEvent.warn(exception, "Unable to start recording on the device with UUID {}", UDID)), + logger.getLoggingEvents()); + } + + @Test + void shouldLogWarnOnStopWithoutThrow() throws MobitruOperationException + { + when(mobitruSessionInfoStorage.getDeviceId()).thenReturn(Optional.of(UDID)); + var exception = new MobitruOperationException(UDID); + doThrow(exception).when(mobitruFacade).stopDeviceScreenRecording(UDID); + var mobitruRecordingListener = new MobitruRecordingListener(true, mobitruFacade, mobitruSessionInfoStorage, + attachmentPublisher); + mobitruRecordingListener.onBeforeSessionStop(mock(BeforeWebDriverQuitEvent.class)); + assertEquals(List.of( + LoggingEvent.warn(exception, + "Unable to retrieve recording from the device with UUID {}", UDID)), + logger.getLoggingEvents()); + } +} diff --git a/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorageTests.java b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorageTests.java new file mode 100644 index 0000000000..c04e2599c1 --- /dev/null +++ b/vividus-plugin-mobitru/src/test/java/org/vividus/mobitru/selenium/MobitruSessionInfoLocalStorageTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.mobitru.selenium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.vividus.selenium.event.AfterWebDriverQuitEvent; +import org.vividus.testcontext.SimpleTestContext; + +class MobitruSessionInfoLocalStorageTests +{ + private static final String DEVICE_ID = "device-id"; + + private final MobitruSessionInfoLocalStorage storage = new MobitruSessionInfoLocalStorage(new SimpleTestContext()); + + @Test + void shouldSaveDeviceId() + { + storage.saveDeviceId(DEVICE_ID); + assertEquals(Optional.of(DEVICE_ID), storage.getDeviceId()); + } + + @Test + void shouldCleanDeviceIdOnSessionStop() + { + storage.saveDeviceId(DEVICE_ID); + storage.onAfterSessionStop(mock(AfterWebDriverQuitEvent.class)); + assertEquals(Optional.empty(), storage.getDeviceId()); + } +} diff --git a/vividus-tests/src/main/resources/properties/suite/system/mobile_app/healthcheck/mobitru/suite.properties b/vividus-tests/src/main/resources/properties/suite/system/mobile_app/healthcheck/mobitru/suite.properties index 08dd312372..7d7c920e13 100644 --- a/vividus-tests/src/main/resources/properties/suite/system/mobile_app/healthcheck/mobitru/suite.properties +++ b/vividus-tests/src/main/resources/properties/suite/system/mobile_app/healthcheck/mobitru/suite.properties @@ -1 +1,3 @@ batch-1.resource-include-patterns=MobitruHealthCheckMobileApp.story + +mobitru.video-recording-enabled=true