diff --git a/build.gradle b/build.gradle index 62106df..96b1e80 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'io.github.lambdatest' -version = '1.0.12' +version = '1.0.13-beta.1' description = 'lambdatest-java-sdk' repositories { @@ -21,7 +21,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' implementation 'io.netty:netty-transport-native-epoll:4.1.104.Final' implementation 'io.netty:netty-transport-native-kqueue:4.1.104.Final' - + implementation 'io.appium:java-client:7.6.0' // New dependencies from POM file implementation 'org.apache.httpcomponents:httpmime:4.5.13' implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' @@ -83,7 +83,7 @@ afterEvaluate { mavenJava(MavenPublication) { groupId = 'io.github.lambdatest' artifactId = 'lambdatest-java-sdk' - version = '1.0.12' + version = '1.0.13-beta.1' pom { name.set('LambdaTest Java SDK') diff --git a/pom.xml b/pom.xml index 24e6dc5..93849e7 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ 4.0.0 io.github.lambdatest lambdatest-java-sdk - 1.0.12 + 1.0.13-beta.1 lambdatest-java-sdk LambdaTest SDK in Java https://www.lambdatest.com @@ -58,6 +58,11 @@ selenium-java 4.1.2 + + io.appium + java-client + 8.0.0 + com.google.code.gson gson diff --git a/src/main/java/io/github/lambdatest/SmartUIAppSnapshot.java b/src/main/java/io/github/lambdatest/SmartUIAppSnapshot.java index 32c328b..7d2258f 100644 --- a/src/main/java/io/github/lambdatest/SmartUIAppSnapshot.java +++ b/src/main/java/io/github/lambdatest/SmartUIAppSnapshot.java @@ -3,17 +3,13 @@ import com.google.gson.Gson; import io.github.lambdatest.constants.Constants; import io.github.lambdatest.models.*; +import io.github.lambdatest.utils.FullPageScreenshotUtil; import io.github.lambdatest.utils.GitUtils; import io.github.lambdatest.utils.SmartUIUtil; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; +import org.openqa.selenium.*; import java.io.File; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.logging.Logger; import io.github.lambdatest.utils.LoggerUtil; @@ -81,67 +77,124 @@ private String getProjectToken(Map options) { } throw new IllegalArgumentException(Constants.Errors.PROJECT_TOKEN_UNSET); } + private void validateMandatoryParams(WebDriver driver, String screenshotName, String deviceName) { + if (driver == null) { + log.severe(Constants.Errors.SELENIUM_DRIVER_NULL + " during take snapshot"); + throw new IllegalArgumentException(Constants.Errors.SELENIUM_DRIVER_NULL); + } + if (screenshotName == null || screenshotName.isEmpty()) { + log.info(Constants.Errors.SNAPSHOT_NAME_NULL); + throw new IllegalArgumentException(Constants.Errors.SNAPSHOT_NAME_NULL); + } + if (deviceName == null || deviceName.isEmpty()) { + throw new IllegalArgumentException(Constants.Errors.DEVICE_NAME_NULL); + } + } + + private String getOptionValue(Map options, String key) { + if (options != null && options.containsKey(key)) { + String value = options.get(key); + return value != null ? value.trim() : ""; + } + return ""; + } + + private UploadSnapshotRequest initializeUploadRequest(String screenshotName, String viewport) { + UploadSnapshotRequest request = new UploadSnapshotRequest(); + request.setScreenshotName(screenshotName); + request.setProjectToken(projectToken); + request.setViewport(viewport); + log.info("Viewport set to :" + viewport); + if (Objects.nonNull(buildData)) { + request.setBuildId(buildData.getBuildId()); + request.setBuildName(buildData.getName()); + } + return request; + } + + private UploadSnapshotRequest configureDeviceNameAndPlatform(UploadSnapshotRequest request, String deviceName, String platform) { + String browserName = deviceName.toLowerCase().startsWith("i") ? "iOS" : "Android"; + String platformName = (platform == null || platform.isEmpty()) ? browserName : platform; + request.setOs(platformName); + request.setDeviceName(deviceName + " " + platformName); + assert platform != null; + request.setBrowserName(platform.toLowerCase().contains("ios") ? "safari" : "chrome"); + return request; + } - public void smartuiAppSnapshot(WebDriver appiumDriver, String screenshotName, Map options) + public void smartuiAppSnapshot(WebDriver driver, String screenshotName, Map options) throws Exception { try { - if (appiumDriver == null) { - log.severe(Constants.Errors.SELENIUM_DRIVER_NULL + " during take snapshot"); - throw new IllegalArgumentException(Constants.Errors.SELENIUM_DRIVER_NULL); + String deviceName = getOptionValue(options, "deviceName"); + String platform = getOptionValue(options, "platform"); + validateMandatoryParams(driver, screenshotName, deviceName); + Dimension d = driver.manage().window().getSize(); + int width = d.getWidth(), height = d.getHeight(); + UploadSnapshotRequest initReq = initializeUploadRequest(screenshotName, width + "x" + height); + UploadSnapshotRequest uploadSnapshotRequest = configureDeviceNameAndPlatform(initReq, deviceName, platform); + String screenshotHash = UUID.randomUUID().toString(); + uploadSnapshotRequest.setScreenshotHash(screenshotHash); + String uploadChunk = getOptionValue(options, "uploadChunk"); + String pageCount = getOptionValue(options, "pageCount"); int userInputtedPageCount=0; + if(!pageCount.isEmpty()) { + userInputtedPageCount = Integer.parseInt(pageCount); } - if (screenshotName == null || screenshotName.isEmpty()) { - log.info(Constants.Errors.SNAPSHOT_NAME_NULL); - throw new IllegalArgumentException(Constants.Errors.SNAPSHOT_NAME_NULL); + if(!uploadChunk.isEmpty() && uploadChunk.toLowerCase().contains("true")) { + uploadSnapshotRequest.setUploadChunk("true"); + } else { + uploadSnapshotRequest.setUploadChunk("false"); } + String navBarHeight = getOptionValue(options, "navigationBarHeight"); + String statusBarHeight = getOptionValue(options, "statusBarHeight"); - TakesScreenshot takesScreenshot = (TakesScreenshot) appiumDriver; - File screenshot = takesScreenshot.getScreenshotAs(OutputType.FILE); - log.info("Screenshot captured: " + screenshotName); - - UploadSnapshotRequest uploadSnapshotRequest = new UploadSnapshotRequest(); - uploadSnapshotRequest.setScreenshotName(screenshotName); - uploadSnapshotRequest.setProjectToken(projectToken); - Dimension d = appiumDriver.manage().window().getSize(); - int w = d.getWidth(), h = d.getHeight(); - uploadSnapshotRequest.setViewport(w + "x" + h); - log.info("Device viewport set to: " + uploadSnapshotRequest.getViewport()); - String platform = "", deviceName = "", browserName = ""; - if (options != null && options.containsKey("platform")) { - platform = options.get("platform").trim(); - } - if (options != null && options.containsKey("deviceName")) { - deviceName = options.get("deviceName").trim(); + if(!navBarHeight.isEmpty()) { + uploadSnapshotRequest.setNavigationBarHeight(navBarHeight); } - if (deviceName == null || deviceName.isEmpty()) { - throw new IllegalArgumentException(Constants.Errors.DEVICE_NAME_NULL); + if(!statusBarHeight.isEmpty()) { + uploadSnapshotRequest.setStatusBarHeight(statusBarHeight); } - if (platform == null || platform.isEmpty()) { - if (deviceName.toLowerCase().startsWith("i")) { - browserName = "iOS"; - } else { - browserName = "Android"; - } + String cropFooter = getOptionValue(options, "cropFooter"); + if (!cropFooter.isEmpty()) { + uploadSnapshotRequest.setCropFooter(cropFooter.toLowerCase()); } - uploadSnapshotRequest.setOs(platform != null && !platform.isEmpty() ? platform : browserName); - if (platform != null && !platform.isEmpty()) { - uploadSnapshotRequest.setDeviceName(deviceName + " " + platform); - } else { - uploadSnapshotRequest.setDeviceName(deviceName + " " + browserName); + String cropStatusBar = getOptionValue(options, "cropStatusBar"); + if (!cropStatusBar.isEmpty()) { + uploadSnapshotRequest.setCropStatusBar(cropStatusBar.toLowerCase()); } - if (platform.toLowerCase().contains("ios")) { - uploadSnapshotRequest.setBrowserName("safari"); + String fullPage = getOptionValue(options, "fullPage").toLowerCase(); + if(!Boolean.parseBoolean(fullPage)){ + if(!pageCount.isEmpty()){ + throw new IllegalArgumentException(Constants.Errors.PAGE_COUNT_ERROR); + } + TakesScreenshot takesScreenshot = (TakesScreenshot) driver; + File screenshot = takesScreenshot.getScreenshotAs(OutputType.FILE); + log.info("Screenshot captured: " + screenshotName); + uploadSnapshotRequest.setFullPage("false"); + util.uploadScreenshot(screenshot, uploadSnapshotRequest, this.buildData); + } else { - uploadSnapshotRequest.setBrowserName("chrome"); - } - if (Objects.nonNull(buildData)) { - uploadSnapshotRequest.setBuildId(buildData.getBuildId()); - uploadSnapshotRequest.setBuildName(buildData.getName()); + uploadSnapshotRequest.setFullPage("true"); + FullPageScreenshotUtil fullPageCapture = new FullPageScreenshotUtil(driver, screenshotName); + List ssDir = fullPageCapture.captureFullPage(userInputtedPageCount); + if(ssDir.isEmpty()){ + throw new RuntimeException(Constants.Errors.SMARTUI_SNAPSHOT_FAILED); + } + int pageCountInSsDir = ssDir.size(); int i; + if(pageCountInSsDir == 1) { //when page count is set to 1 as user for fullPage + uploadSnapshotRequest.setFullPage("false"); + util.uploadScreenshot(ssDir.get(0), uploadSnapshotRequest, this.buildData); + return; + } + for( i = 0; i < pageCountInSsDir -1; ++i){ + uploadSnapshotRequest.setIsLastChunk("false"); + uploadSnapshotRequest.setChunkCount(i); + util.uploadScreenshot(ssDir.get(i), uploadSnapshotRequest, this.buildData); + } + uploadSnapshotRequest.setIsLastChunk("true"); + uploadSnapshotRequest.setChunkCount(i); + util.uploadScreenshot(ssDir.get(pageCountInSsDir-1), uploadSnapshotRequest, this.buildData); } - UploadSnapshotResponse uploadSnapshotResponse = util.uploadScreenshot(screenshot, uploadSnapshotRequest, - this.buildData); - log.info("For uploading: " + uploadSnapshotRequest.toString() + " received response: " - + uploadSnapshotResponse.getData()); } catch (Exception e) { log.severe(Constants.Errors.UPLOAD_SNAPSHOT_FAILED + " due to: " + e.getMessage()); throw new Exception("Couldnt upload image to Smart UI due to: " + e.getMessage()); diff --git a/src/main/java/io/github/lambdatest/constants/Constants.java b/src/main/java/io/github/lambdatest/constants/Constants.java index aa47ff7..6e4d423 100644 --- a/src/main/java/io/github/lambdatest/constants/Constants.java +++ b/src/main/java/io/github/lambdatest/constants/Constants.java @@ -1,14 +1,22 @@ package io.github.lambdatest.constants; +import static io.github.lambdatest.constants.Constants.SmartUIRoutes.SMARTUI_CLIENT_API_URL; + public interface Constants { String SMARTUI_SERVER_ADDRESS = "SMARTUI_SERVER_ADDRESS"; public static final String PROJECT_TOKEN = "projectToken"; + public final String TEST_TYPE = "lambdatest-java-app-sdk"; String LOCAL_SERVER_HOST = "http://localhost:8080"; + public static String getHostUrlFromEnvOrDefault() { + String envUrl = System.getenv("SMARTUI_CLIENT_API_URL"); + return (envUrl != null && !envUrl.isEmpty()) ? envUrl : SMARTUI_CLIENT_API_URL; + } + //SmartUI API routes interface SmartUIRoutes { - public static final String HOST_URL = "https://api.lambdatest.com/visualui/1.0"; + public static final String SMARTUI_CLIENT_API_URL = "https://api.lambdatest.com/visualui/1.0"; public static final String SMARTUI_HEALTHCHECK_ROUTE = "/healthcheck"; public static final String SMARTUI_DOMSERIALIZER_ROUTE = "/domserializer"; public static final String SMARTUI_SNAPSHOT_ROUTE = "/snapshot"; @@ -44,6 +52,7 @@ interface LogEnvVars { interface Errors { public static final String SELENIUM_DRIVER_NULL = "An instance of the selenium driver object is required."; public static final String SNAPSHOT_NAME_NULL = "The `snapshotName` argument is required."; + public static final String SNAPSHOT_NOT_FOUND = "Screenshot not found."; public static final String SMARTUI_NOT_RUNNING = "SmartUI server is not running."; public static final String JAVA_SCRIPT_NOT_SUPPORTED = "The driver does not support JavaScript execution."; public static final String EMPTY_RESPONSE_DOMSERIALIZER = "Response from fetchDOMSerializer is null or empty."; @@ -60,6 +69,7 @@ interface Errors { public static final String PROJECT_TOKEN_UNSET = "projectToken cant be empty"; public static final String USER_AUTH_ERROR = "User authentication failed"; public static final String STOP_BUILD_FAILED = "Failed to stop build"; + public static final String PAGE_COUNT_ERROR = "Page Count Value is invalid"; public static final String NULL_OPTIONS_OBJECT = "Options object is null or missing in request."; public static final String DEVICE_NAME_NULL = "Device name is a mandatory parameter."; } diff --git a/src/main/java/io/github/lambdatest/models/UploadSnapshotRequest.java b/src/main/java/io/github/lambdatest/models/UploadSnapshotRequest.java index 885c922..faa4215 100644 --- a/src/main/java/io/github/lambdatest/models/UploadSnapshotRequest.java +++ b/src/main/java/io/github/lambdatest/models/UploadSnapshotRequest.java @@ -13,7 +13,16 @@ public class UploadSnapshotRequest { private String buildId; private String buildName; private String screenshotName; + private String screenshotHash; private String deviceName; + private String cropFooter; + private String cropStatusBar; + private String fullPage; + private String isLastChunk; + private Integer chunkCount; + private String uploadChunk; + private String navigationBarHeight; + private String statusBarHeight; // Default constructor public UploadSnapshotRequest() { @@ -22,7 +31,9 @@ public UploadSnapshotRequest() { // All Args constructor public UploadSnapshotRequest(String screenshot, String browserName, String os, String viewport, String projectToken, String buildId, String buildName, - String screenshotName, String deviceName) { + String screenshotName, String screenshotHash ,String deviceName,String fullPage, String cropFooter, + String cropStatusBar, String isLastChunk, Integer chunkCount, String uploadChunk, + String navigationBarHeight, String statusBarHeight) { this.browserName = browserName; this.os = os; this.viewport = viewport; @@ -30,7 +41,16 @@ public UploadSnapshotRequest(String screenshot, String browserName, String os, S this.buildId = buildId; this.buildName = buildName; this.screenshotName = screenshotName; + this.screenshotHash = screenshotHash; this.deviceName = deviceName; + this.cropFooter = cropFooter; + this.cropStatusBar = cropStatusBar; + this.fullPage = fullPage; + this.isLastChunk = isLastChunk; + this.chunkCount = chunkCount; + this.uploadChunk = uploadChunk; + this.navigationBarHeight = navigationBarHeight; + this.statusBarHeight = statusBarHeight; } // Getters and setters @@ -54,6 +74,36 @@ public String getViewport() { return viewport; } + public String getCropFooter() { return cropFooter; } + + public void setCropFooter(String cropFooter) { + this.cropFooter = cropFooter; + } + + public String getCropStatusBar() { return cropStatusBar; } + + public String getFullPage() { return fullPage; } + + public void setFullPage(String fullPage) { + this.fullPage = fullPage; + } + + public String getIsLastChunk() { return isLastChunk; } + + public void setIsLastChunk(String isLastChunk) { + this.isLastChunk = isLastChunk; + } + + public String getUploadChunk() { return uploadChunk; } + + public void setUploadChunk(String uploadChunk) { + this.uploadChunk = uploadChunk; + } + + public void setCropStatusBar(String cropStatusBar) { + this.cropStatusBar = cropStatusBar; + } + public void setViewport(String viewport) { this.viewport = viewport; } @@ -90,6 +140,14 @@ public void setScreenshotName(String screenshotName) { this.screenshotName = screenshotName; } + public String getScreenshotHash() { + return screenshotHash; + } + + public void setScreenshotHash(String screenshotHash) { + this.screenshotHash = screenshotHash; + } + public String getDeviceName() { return deviceName; } @@ -97,4 +155,28 @@ public String getDeviceName() { public void setDeviceName(String deviceName) { this.deviceName = deviceName; } + + public void setChunkCount(int chunkCount) { + this.chunkCount = chunkCount; + } + + public Integer getChunkCount() { + return chunkCount; + } + + public String getNavigationBarHeight() { + return navigationBarHeight; + } + + public void setNavigationBarHeight(String navigationBarHeight) { + this.navigationBarHeight = navigationBarHeight; + } + + public String getStatusBarHeight() { + return statusBarHeight; + } + + public void setStatusBarHeight(String statusBarHeight) { + this.statusBarHeight = statusBarHeight; + } } diff --git a/src/main/java/io/github/lambdatest/utils/FullPageScreenshotUtil.java b/src/main/java/io/github/lambdatest/utils/FullPageScreenshotUtil.java new file mode 100644 index 0000000..06d52cf --- /dev/null +++ b/src/main/java/io/github/lambdatest/utils/FullPageScreenshotUtil.java @@ -0,0 +1,153 @@ +package io.github.lambdatest.utils; + +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.PerformsTouchActions; +import io.appium.java_client.TouchAction; +import io.appium.java_client.touch.WaitOptions; +import io.appium.java_client.touch.offset.PointOption; +import org.openqa.selenium.*; +import org.openqa.selenium.interactions.PointerInput; +import org.openqa.selenium.interactions.Sequence; +import org.openqa.selenium.remote.RemoteWebElement; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.Duration; +import java.util.*; +import java.util.logging.Logger; + +public class FullPageScreenshotUtil { + private final WebDriver driver; + private final String saveDirectoryName; + private final Logger log = LoggerUtil.createLogger("lambdatest-java-app-sdk"); + + public FullPageScreenshotUtil(WebDriver driver, String saveDirectoryName) { + this.driver = driver; + this.saveDirectoryName = saveDirectoryName; + + // Ensure the directory exists + File dir = new File(saveDirectoryName); + if (!dir.exists()) { + dir.mkdirs(); + } + } + + private String prevPageSource = ""; + private int samePageCounter = 1; //Init with value 1 , finalise at 3 + private int maxCount = 10; + public List captureFullPage(int pageCount) { + if(pageCount<=0){ + pageCount = maxCount; + } + if (pageCount < maxCount) { + maxCount = pageCount; + } + int chunkCount = 0; + boolean isLastScroll = false; + List screenshotDir = new ArrayList<>(); + while (!isLastScroll && chunkCount < maxCount) { + File screenshotFile= captureAndSaveScreenshot(this.saveDirectoryName,chunkCount); + if(screenshotFile != null) { + screenshotDir.add(screenshotFile); + chunkCount++; + } + //Perform scroll + scrollDown(); + log.info("Scrolling attempt # " + chunkCount); + // Detect end of page + isLastScroll = hasReachedBottom(); + } + log.info("Finished capturing all screenshots for full page."); + return screenshotDir; + } + + private File captureAndSaveScreenshot(String ssDir, int index) { + File destinationFile = new File(ssDir + "/" + ssDir +"_" + index + ".png"); + try { + File screenshotFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); + Files.copy(screenshotFile.toPath(), destinationFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.info("Saved screenshot: " + destinationFile.getAbsolutePath()); + } catch (IOException e) { + log.warning("Error saving screenshot: " + e.getMessage()); + } + return destinationFile; + } + + private void scrollDown() { + Dimension screenSize = driver.manage().window().getSize(); + int screenHeight = screenSize.getHeight(); + int screenWidth = screenSize.getWidth(); + + // Define start and end points for scrolling + int startX = 4; //start from 4 pixels from the left, to avoid click on action items/webview + int startY = (int) (screenHeight * 0.70); // Start at 70% of the screen height + int endY = (int) (screenHeight * 0.45); // Scroll up to 25% + int scrollHeight = startY - endY; + + try { + // Try iOS style swipe + JavascriptExecutor javascriptExecutorIos = (JavascriptExecutor) driver; + Map swipeObj = new HashMap<>(); + swipeObj.put("fromX", startX); + swipeObj.put("fromY", startY); + swipeObj.put("toX", startX); + swipeObj.put("toY", endY); + swipeObj.put("duration", 0.8); + javascriptExecutorIos.executeScript("mobile: dragFromToForDuration", swipeObj); + + } catch (Exception iosException) { + try { + // If iOS swipe fails, assume it's Android and do scrollGesture + JavascriptExecutor jsExecutorAndroid = (JavascriptExecutor) driver; + Map scrollParams = new HashMap<>(); + scrollParams.put("left", startX); + scrollParams.put("top", endY); + scrollParams.put("width", screenWidth - startX); + scrollParams.put("height", scrollHeight); + scrollParams.put("direction", "down"); + scrollParams.put("percent", 1.0); + scrollParams.put("speed", 2500); + jsExecutorAndroid.executeScript("mobile:scrollGesture", scrollParams); + } catch (Exception e) { + log.warning("Error during Android scroll operation: " + e.getMessage()); + e.printStackTrace(); + } + } + + try { + Thread.sleep(1000); + } catch (Exception e) { + log.warning("Error during scroll operation: " + e.getMessage()); + e.printStackTrace(); + } + } + + private boolean hasReachedBottom() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + String currentPageSource = driver.getPageSource(); + if (currentPageSource == null) { + log.warning("Page source is null"); + return false; + } + if (currentPageSource.equals(prevPageSource)) { + samePageCounter++; + log.info("Same page content detected, counter: " + samePageCounter); + if (samePageCounter >= 3) { + log.info("Reached the bottom of the page — no new content found."); + samePageCounter = 0; + return true; + } + } else { + prevPageSource = currentPageSource; + samePageCounter = 0; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lambdatest/utils/GitUtils.java b/src/main/java/io/github/lambdatest/utils/GitUtils.java index 92cdd09..fd8b1e6 100644 --- a/src/main/java/io/github/lambdatest/utils/GitUtils.java +++ b/src/main/java/io/github/lambdatest/utils/GitUtils.java @@ -7,14 +7,9 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; -import java.util.Arrays; -import java.util.List; -import java.util.ArrayList; -import java.util.Map; -import io.github.lambdatest.utils.LoggerUtil; +import java.util.*; import java.util.logging.Logger; -import java.util.stream.Collectors; public class GitUtils { @@ -25,7 +20,8 @@ public static GitInfo getGitInfo(Map envVars) { if (gitInfoFilePath != null) { return readGitInfoFromFile(gitInfoFilePath, envVars); } else { - return fetchGitInfoFromCommands(envVars); + GitInfo gitInfo = fetchGitInfoFromCommands(envVars); + return gitInfo; } } @@ -55,11 +51,10 @@ private static GitInfo fetchGitInfoFromCommands(Map envVars) { String command = String.format( "git log -1 --pretty=format:\"%s\" && git rev-parse --abbrev-ref HEAD && git tag --contains HEAD", String.join(splitCharacter, prettyFormat)); - List outputLines = executeCommand(command); if (outputLines.isEmpty()) { - return null; + return new GitInfo("", "", "", "", "", ""); } String[] res = String.join("\n", outputLines).split(splitCharacter); @@ -91,14 +86,39 @@ private static String getGitHubURL(Map envVars, String commitId) } private static List executeCommand(String command) { + List outputLines = new ArrayList<>(); try { - Process process = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", command }); - return new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines() - .collect(Collectors.toList()); - } catch (IOException e) { + String os = System.getProperty("os.name").toLowerCase(); + Process process; + if (os.contains("win")) { + // For Windows, use cmd.exe to execute the command + process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", command }); + } else { + // For Unix-like systems (Linux, macOS), use /bin/sh + process = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", command }); + } + // Read both the output and error streams + try (BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + // Capture standard output (stdout) + while ((line = inputReader.readLine()) != null) { + outputLines.add(line); + } + // Capture error output (stderr) + while ((line = errorReader.readLine()) != null) { + log.severe("Error: " + line); + } + } + // Wait for the command to complete + int exitCode = process.waitFor(); + if (exitCode != 0) { + log.severe("Command failed with exit code: " + exitCode); + } + } catch (IOException | InterruptedException e) { log.severe("Error executing command: " + e.getMessage()); return new ArrayList(); } + return outputLines; } } diff --git a/src/main/java/io/github/lambdatest/utils/HttpClientUtil.java b/src/main/java/io/github/lambdatest/utils/HttpClientUtil.java index e5fb583..f229204 100644 --- a/src/main/java/io/github/lambdatest/utils/HttpClientUtil.java +++ b/src/main/java/io/github/lambdatest/utils/HttpClientUtil.java @@ -25,20 +25,18 @@ import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Objects; -import java.util.logging.Level; import java.util.logging.Logger; -import io.github.lambdatest.utils.LoggerUtil; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.TrustAllStrategy; import org.apache.http.conn.ssl.NoopHostnameVerifier; import javax.net.ssl.SSLContext; +import static io.github.lambdatest.constants.Constants.TEST_TYPE; + public class HttpClientUtil { private final CloseableHttpClient httpClient; private Logger log = LoggerUtil.createLogger("lambdatest-java-sdk"); @@ -255,7 +253,8 @@ private void checkResponseStatus(HttpResponse response) throws IOException { public boolean isUserAuthenticated(String projectToken) throws Exception { try { - String url = Constants.SmartUIRoutes.HOST_URL + Constants.SmartUIRoutes.SMARTUI_AUTH_ROUTE; + String hostUrl = Constants.getHostUrlFromEnvOrDefault(); + String url = hostUrl + Constants.SmartUIRoutes.SMARTUI_AUTH_ROUTE; HttpGet request = new HttpGet(url); request.setHeader(Constants.PROJECT_TOKEN, projectToken); log.info("Authenticating user for projectToken :" + projectToken); @@ -295,7 +294,8 @@ public String postSnapshot(String data) throws IOException { } public String createSmartUIBuild(String createBuildRequest, Map headers) throws IOException { - return postWithHeader(Constants.SmartUIRoutes.HOST_URL + Constants.SmartUIRoutes.SMARTUI_CREATE_BUILD, + String hostUrl = Constants.getHostUrlFromEnvOrDefault(); + return postWithHeader(hostUrl + Constants.SmartUIRoutes.SMARTUI_CREATE_BUILD, createBuildRequest, headers); } @@ -307,46 +307,109 @@ public void stopBuild(String buildId, Map headers) throws IOExce headers.put(Constants.PROJECT_TOKEN, projectToken); } } + String hostUrl = Constants.getHostUrlFromEnvOrDefault(); String response = delete( - Constants.SmartUIRoutes.HOST_URL + Constants.SmartUIRoutes.SMARTUI_FINALISE_BUILD_ROUTE + buildId, + hostUrl + Constants.SmartUIRoutes.SMARTUI_FINALISE_BUILD_ROUTE + buildId + "&testType="+ TEST_TYPE, headers); } - public String uploadScreenshot(String url, File screenshot, UploadSnapshotRequest uploadScreenshotRequest, - BuildData data) throws IOException { + public String uploadScreenshot(String url, File screenshot, UploadSnapshotRequest request, + BuildData data) throws IOException { + HttpPost uploadRequest = new HttpPost(url); + uploadRequest.setHeader("projectToken", request.getProjectToken()); + + // Build the multipart request entity + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.STRICT); + + // Add the required fields + builder.addBinaryBody("screenshot", screenshot, ContentType.create("image/png"), request.getScreenshotName()); + builder.addTextBody("buildId", data.getBuildId()); + builder.addTextBody("buildName", data.getName()); + builder.addTextBody("baseline", Boolean.toString(data.getBaseline())); + builder.addTextBody("screenshotName", request.getScreenshotName()); + builder.addTextBody("browser", request.getBrowserName()); + builder.addTextBody("deviceName", request.getDeviceName()); + builder.addTextBody("os", request.getOs()); + builder.addTextBody("viewport", request.getViewport()); + builder.addTextBody("uploadChunk", request.getUploadChunk()); + builder.addTextBody("projectType", TEST_TYPE); + builder.addTextBody("screenshotHash", request.getScreenshotHash()); + + // Add optional fields if present + if (Objects.nonNull(request.getFullPage())) { + builder.addTextBody("fullPage", request.getFullPage()); + } + if (Objects.nonNull(request.getIsLastChunk())) { + builder.addTextBody("isLastChunk", request.getIsLastChunk()); + } + if (Objects.nonNull(request.getChunkCount())) { + builder.addTextBody("chunkCount", String.valueOf(request.getChunkCount())); + } - try { - HttpPost uploadRequest = new HttpPost(url); - uploadRequest.setHeader("projectToken", uploadScreenshotRequest.getProjectToken()); - - MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.setMode(HttpMultipartMode.STRICT); - - builder.addBinaryBody("screenshot", screenshot, ContentType.create("image/png"), screenshot.getName()); - builder.addTextBody("buildId", uploadScreenshotRequest.getBuildId()); - builder.addTextBody("buildName", uploadScreenshotRequest.getBuildName()); - builder.addTextBody("screenshotName", uploadScreenshotRequest.getScreenshotName()); - builder.addTextBody("browser", uploadScreenshotRequest.getBrowserName()); - builder.addTextBody("deviceName", uploadScreenshotRequest.getDeviceName()); - builder.addTextBody("os", uploadScreenshotRequest.getOs()); - builder.addTextBody("viewport", uploadScreenshotRequest.getViewport()); - builder.addTextBody("projectType", "lambdatest-java-app-sdk"); - if (data.getBaseline()) { - builder.addTextBody("baseline", "true"); - } else { - builder.addTextBody("baseline", "false"); + // Handle status bar height + String statusBarHeight = ""; + if (request.getStatusBarHeight() == null) { + builder.addTextBody("statusBarHeight", statusBarHeight); + } else { + statusBarHeight = request.getStatusBarHeight(); + //only set cropStatusBar to false when it exists and statusBarHeight is valid + if (request.getCropStatusBar() != null && Boolean.parseBoolean(request.getCropStatusBar()) + && isValidNumber(statusBarHeight)) { + request.setCropStatusBar("false"); // Overwrite since we have custom value from user } + request.setCropStatusBar("false"); + builder.addTextBody("statusBarHeight", statusBarHeight); + } - HttpEntity multipart = builder.build(); - uploadRequest.setEntity(multipart); + if (request.getCropStatusBar() != null) { + builder.addTextBody("cropStatusBar", request.getCropStatusBar()); + } - try (CloseableHttpResponse response = httpClient.execute(uploadRequest)) { - return EntityUtils.toString(response.getEntity()); + // Handle navigation bar height + String navigationBarHeight = ""; + if (request.getNavigationBarHeight() == null) { + builder.addTextBody("navigationBarHeight", navigationBarHeight); + } else { + navigationBarHeight = request.getNavigationBarHeight(); + //only set cropFooter to false when it exists and navigationBarHeight is valid + if (request.getCropFooter() != null && Boolean.parseBoolean(request.getCropFooter()) + && isValidNumber(navigationBarHeight)) { + request.setCropFooter("false"); // Overwrite since we have custom value from user } + request.setCropFooter("false"); + builder.addTextBody("navigationBarHeight", navigationBarHeight); + } + + if (request.getCropFooter() != null) { + builder.addTextBody("cropFooter", request.getCropFooter()); + } + + // Execute the request + HttpEntity multipart = builder.build(); + uploadRequest.setEntity(multipart); + try (CloseableHttpResponse response = httpClient.execute(uploadRequest)) { + return EntityUtils.toString(response.getEntity()); } catch (IOException e) { - log.warning("Exception occurred in uploading screenshot: " + - e.getMessage()); - return "An error occurred while processing your request."; + + log.warning("Exception occurred in uploading screenshot: " + e.getMessage()); + throw new IOException("Failed to upload screenshot", e); + } + } + + private boolean isValidNumber(String value) { + if (value == null || value.isEmpty()) { + return false; + } + try { + int strVal = Integer.parseInt(value); + if(strVal >=1) { + return true; + } else { + throw new NumberFormatException("Invalid value for cropping, pls provide a valid value"); + } + } catch (NumberFormatException e) { + return false; } } diff --git a/src/main/java/io/github/lambdatest/utils/SmartUIUtil.java b/src/main/java/io/github/lambdatest/utils/SmartUIUtil.java index 62da079..330f788 100644 --- a/src/main/java/io/github/lambdatest/utils/SmartUIUtil.java +++ b/src/main/java/io/github/lambdatest/utils/SmartUIUtil.java @@ -2,13 +2,13 @@ import java.io.File; import java.util.*; -import java.util.logging.Level; import java.util.logging.Logger; import io.github.lambdatest.models.*; import com.google.gson.Gson; import io.github.lambdatest.constants.Constants; + public class SmartUIUtil { private final HttpClientUtil httpClient; private final Logger log = LoggerUtil.createLogger("lambdatest-java-sdk"); @@ -86,20 +86,22 @@ public static String getSmartUIServerAddress() { } } - public UploadSnapshotResponse uploadScreenshot(File screenshotFile, UploadSnapshotRequest uploadScreenshotRequest, - BuildData buildData) throws Exception { + public void uploadScreenshot(File screenshotFile, UploadSnapshotRequest uploadScreenshotRequest, + BuildData buildData) throws Exception { UploadSnapshotResponse uploadAPIResponse = new UploadSnapshotResponse(); try { - String url = Constants.SmartUIRoutes.HOST_URL + Constants.SmartUIRoutes.SMARTUI_UPLOAD_SCREENSHOT_ROUTE; - String uploadScreenshotResponse = httpClient.uploadScreenshot(url, screenshotFile, uploadScreenshotRequest, - buildData); + if(Objects.isNull(screenshotFile)){ + throw new RuntimeException(Constants.Errors.SNAPSHOT_NOT_FOUND); + } + String hostUrl = Constants.getHostUrlFromEnvOrDefault(); + String url = hostUrl + Constants.SmartUIRoutes.SMARTUI_UPLOAD_SCREENSHOT_ROUTE; + String uploadScreenshotResponse = httpClient.uploadScreenshot(url, screenshotFile, uploadScreenshotRequest, buildData); uploadAPIResponse = gson.fromJson(uploadScreenshotResponse, UploadSnapshotResponse.class); if (Objects.isNull(uploadAPIResponse)) throw new IllegalStateException("Failed to upload screenshot to SmartUI"); } catch (Exception e) { throw new Exception("Couldn't upload image to SmartUI because of error : " + e.getMessage()); } - return uploadAPIResponse; } public BuildResponse build(GitInfo git, String projectToken, Map options) throws Exception { @@ -112,7 +114,6 @@ public BuildResponse build(GitInfo git, String projectToken, Map if (options != null && options.containsKey("buildName")) { String buildNameStr = options.get("buildName"); - // Check if value is non-null and a valid String if (buildNameStr != null && !buildNameStr.trim().isEmpty()) { createBuildRequest.setBuildName(buildNameStr); log.info("Build name set from options: " + buildNameStr); @@ -124,11 +125,12 @@ public BuildResponse build(GitInfo git, String projectToken, Map } else { createBuildRequest.setBuildName("smartui-" + UUID.randomUUID().toString().substring(0, 10)); } + if (Objects.nonNull(git)) { createBuildRequest.setGit(git); } String createBuildJson = gson.toJson(createBuildRequest); - Map header = new HashMap(); + Map header = new HashMap<>(); header.put(Constants.PROJECT_TOKEN, projectToken); String createBuildResponse = httpClient.createSmartUIBuild(createBuildJson, header); BuildResponse buildData = gson.fromJson(createBuildResponse, BuildResponse.class); @@ -140,7 +142,7 @@ public BuildResponse build(GitInfo git, String projectToken, Map public void stopBuild(String buildId, String projectToken) throws Exception { try { - Map headers = new HashMap(); + Map headers = new HashMap<>(); headers.put(Constants.PROJECT_TOKEN, projectToken); httpClient.stopBuild(buildId, headers); } catch (Exception e) {