Skip to content

Release 1.0.13 #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
afde02a
Added crop snapshot for SmartUIAppSnapshot
greydaemon Mar 28, 2025
835dd89
sdk changes for crop status and navigation bar
greydaemon Apr 3, 2025
8933261
Merge pull request #27 from greydaemon/DOT-4926
sushobhit-lt Apr 4, 2025
18546da
added null check for safety
greydaemon Apr 4, 2025
ca8c3b7
Merge pull request #28 from greydaemon/DOT-4926
sushobhit-lt Apr 4, 2025
9211130
DOT-4925 : Changes for full page ss in app sdk
greydaemon Apr 7, 2025
4996dc5
minor refactor
greydaemon Apr 7, 2025
4ce5324
[DOT-4925] Changes and refactoring for supporting full page screensho…
greydaemon Apr 8, 2025
f29d8ea
[DOT-4925] Added support for exporting host url and support for Uploa…
greydaemon Apr 9, 2025
84ffa5f
changed scrolling logic to scroll by 20%
greydaemon Apr 9, 2025
77e13de
Refactored changes as per testing
greydaemon Apr 10, 2025
fada3e7
minor refactor
greydaemon Apr 10, 2025
2552de7
Changes as per requirement changes for custom cropping and QA testing
greydaemon Apr 14, 2025
eabd753
changes in stop build and http client
greydaemon Apr 14, 2025
c417003
removed unused imports and minor refactoring in http client util
greydaemon Apr 15, 2025
c1e35f8
removed logs for debugging and handled edge case in pageCount
greydaemon Apr 15, 2025
3483e7e
Merge pull request #29 from greydaemon/DOT-4925
sushobhit-lt Apr 17, 2025
c9c2ae8
bump version
sushobhit-lt Apr 18, 2025
e2e4581
Merge pull request #32 from sushobhit-lt/stage
sushobhit-lt Apr 18, 2025
0aff2b2
downgraded appium version to support GS dependency conflict
greydaemon Apr 21, 2025
38950cf
Merge remote-tracking branch 'upstream/stage' into DOT-4925
greydaemon Apr 21, 2025
8a35bb2
Added beta version for testing conflicting dependencies for GS test s…
greydaemon Apr 21, 2025
67eed71
bumped sdk version to 13.1 beta
greydaemon Apr 21, 2025
37ee7b1
Merge pull request #33 from greydaemon/DOT-4925
sushobhit-lt Apr 21, 2025
823378c
made changes in scrolling logic to support appium version downgrade f…
greydaemon Apr 22, 2025
6a4c6f8
introduced some delay in swipe for IOS devices
greydaemon Apr 22, 2025
ace5e35
changes in sdk version to beta 1
greydaemon Apr 22, 2025
6e39895
Merge pull request #34 from greydaemon/DOT-4925
sushobhit-lt Apr 22, 2025
27385b3
DOT-5197 : Handled scrolling issue for Goldman on Perfecto
greydaemon Apr 29, 2025
64b6788
Changes in catch block, removed warning logs
greydaemon Apr 29, 2025
4265a0f
Final changes in scrolling as per testing on LT, and perfecto
greydaemon Apr 30, 2025
1e82403
Merge pull request #35 from greydaemon/DOT-5197
sushobhit-lt Apr 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ plugins {
}

group = 'io.github.lambdatest'
version = '1.0.12'
version = '1.0.14-beta.1'
description = 'lambdatest-java-sdk'

repositories {
@@ -17,12 +17,11 @@ repositories {
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
implementation 'org.json:json:20231013'
compileOnly 'org.seleniumhq.selenium:selenium-java:[4.0.0,)'
// compileOnly 'org.seleniumhq.selenium:selenium-java:[4.0.0,)'
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'

// New dependencies from POM file
implementation 'io.appium:java-client:7.6.0'
implementation 'org.apache.httpcomponents:httpmime:4.5.13'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1'
}
@@ -83,7 +82,7 @@ afterEvaluate {
mavenJava(MavenPublication) {
groupId = 'io.github.lambdatest'
artifactId = 'lambdatest-java-sdk'
version = '1.0.12'
version = '1.0.14-beta.1'

pom {
name.set('LambdaTest Java SDK')
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.lambdatest</groupId>
<artifactId>lambdatest-java-sdk</artifactId>
<version>1.0.12</version>
<version>1.0.14-beta.1</version>
<name>lambdatest-java-sdk</name>
<description>LambdaTest SDK in Java</description>
<url>https://www.lambdatest.com</url>
@@ -58,6 +58,11 @@
<artifactId>selenium-java</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>8.0.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
163 changes: 108 additions & 55 deletions src/main/java/io/github/lambdatest/SmartUIAppSnapshot.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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<String, String> options)
public void smartuiAppSnapshot(WebDriver driver, String screenshotName, Map<String, String> 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<File> 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());
14 changes: 12 additions & 2 deletions src/main/java/io/github/lambdatest/constants/Constants.java
Original file line number Diff line number Diff line change
@@ -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.";
@@ -56,10 +65,11 @@ interface Errors {
public static final String POST_SNAPSHOT_FAILED = "Post snapshot failed: %s";
public static final String UPLOAD_SNAPSHOT_FAILED = "Upload snapshot failed: ";
public static final String INVALID_RESPONSE_DATA = "Invalid response from fetchDOMSerializer";
public static final String SMARTUI_SNAPSHOT_FAILED = "SmartUI snapshot failed %s";
public static final String SMARTUI_SNAPSHOT_FAILED = "SmartUI snapshot failed";
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.";
}
Original file line number Diff line number Diff line change
@@ -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,15 +31,26 @@ 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;
this.projectToken = projectToken;
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,11 +140,43 @@ 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;
}

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;
}
}
173 changes: 173 additions & 0 deletions src/main/java/io/github/lambdatest/utils/FullPageScreenshotUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package io.github.lambdatest.utils;

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileBy;
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 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;
private int maxCount = 10;
public List<File> captureFullPage(int pageCount) {
if(pageCount<=0){
pageCount = maxCount;
}
if (pageCount < maxCount) {
maxCount = pageCount;
}
int chunkCount = 0;
boolean isLastScroll = false;
List<File> 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() {
try {
Thread.sleep(1000);
scrollOnPerfecto((AppiumDriver) driver);
} catch (Exception e) {
log.warning("Error during scroll operation: " + e.getMessage());
e.printStackTrace();
}
}

public void scrollOnPerfecto(AppiumDriver driver) {
try {
Dimension size = driver.manage().window().getSize();
int screenHeight = size.getHeight();
int screenWidth = size.getWidth();
int startX = 4;
int startY = (int) (screenHeight * 0.70);
int endY = (int) (screenHeight * 0.45);
int scrollHeight = startY - endY;

// First try Perfecto's native scroll command
try {
Map<String, Object> params = new HashMap<>();
params.put("start", "50%,80%");
params.put("end", "50%,20%");
params.put("duration", "2");

driver.executeScript("mobile:touch:swipe", params);
log.info("Perfecto scroll command succeeded");
return;
} catch (Exception ignore) {}

//Trying Scroll Gestures to scroll - supported on Android devices
try {
Map<String, Object> 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);
((JavascriptExecutor) driver).executeScript("mobile:scrollGesture", scrollParams);
log.info("ScrollGestures scroll succeeded");
} catch (Exception ignore) {}

//Try ios style swipe - supported on IOS devices
try {
Map<String, Object> swipeObj = new HashMap<>();
swipeObj.put("fromX", startX);
swipeObj.put("fromY", startY);
swipeObj.put("toX", startX);
swipeObj.put("toY", endY);
swipeObj.put("duration", 0.8);
((JavascriptExecutor) driver).executeScript("mobile: dragFromToForDuration", swipeObj);
log.info("DragFromTo scroll succeeded");
} catch (Exception ignore) {}
// Fallback to TouchAction if none work
try {
TouchAction touchAction = new TouchAction(driver);
touchAction.press(PointOption.point(startX, startY))
.waitAction(WaitOptions.waitOptions(Duration.ofMillis(1000))) // Longer wait for Perfecto
.moveTo(PointOption.point(startX, endY))
.release()
.perform();
log.info("TouchAction scroll succeeded");
} catch (Exception ignored) {}
} catch (Exception e) {
log.severe("Scroll not supported on this device : " + e.getMessage());
}
}

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;
}
}
48 changes: 34 additions & 14 deletions src/main/java/io/github/lambdatest/utils/GitUtils.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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<String> 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<String, String> envVars, String commitId)
}

private static List<String> executeCommand(String command) {
List<String> 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<String>();
}
return outputLines;
}
}
135 changes: 99 additions & 36 deletions src/main/java/io/github/lambdatest/utils/HttpClientUtil.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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;
}
}

22 changes: 12 additions & 10 deletions src/main/java/io/github/lambdatest/utils/SmartUIUtil.java
Original file line number Diff line number Diff line change
@@ -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<String, String> options) throws Exception {
@@ -112,7 +114,6 @@ public BuildResponse build(GitInfo git, String projectToken, Map<String, String>
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<String, String>
} else {
createBuildRequest.setBuildName("smartui-" + UUID.randomUUID().toString().substring(0, 10));
}

if (Objects.nonNull(git)) {
createBuildRequest.setGit(git);
}
String createBuildJson = gson.toJson(createBuildRequest);
Map<String, String> header = new HashMap<String, String>();
Map<String, String> 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<String, String>

public void stopBuild(String buildId, String projectToken) throws Exception {
try {
Map<String, String> headers = new HashMap<String, String>();
Map<String, String> headers = new HashMap<>();
headers.put(Constants.PROJECT_TOKEN, projectToken);
httpClient.stopBuild(buildId, headers);
} catch (Exception e) {