Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
124 changes: 124 additions & 0 deletions string_to_image_action/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.testsigma.addons</groupId>
<artifactId>string_to_image_action</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<testsigma.sdk.version>1.2.24_cloud</testsigma.sdk.version>
<junit.jupiter.version>5.8.0-M1</junit.jupiter.version>
<testsigma.addon.maven.plugin>1.0.0</testsigma.addon.maven.plugin>
<maven.source.plugin.version>3.2.1</maven.source.plugin.version>
<lombok.version>1.18.30</lombok.version>

</properties>

<dependencies>
<dependency>
<groupId>com.testsigma</groupId>
<artifactId>testsigma-java-sdk</artifactId>
<version>${testsigma.sdk.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.3</version>
</dependency>
Comment on lines +42 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

testng is missing <scope>test</scope> and will be bundled into the shaded JAR.

TestNG is a test framework and should not be a compile/runtime dependency. Additionally, version 6.14.3 is from 2018; if TestNG is required at all, a current version should be used.

♻️ Proposed fix
 <dependency>
     <groupId>org.testng</groupId>
     <artifactId>testng</artifactId>
-    <version>6.14.3</version>
+    <version>7.9.0</version>
+    <scope>test</scope>
 </dependency>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@string_to_image_action/pom.xml` around lines 42 - 46, The TestNG dependency
(org.testng:testng version 6.14.3) is declared without a test scope and will be
packaged into the shaded JAR; update the dependency declaration to add
<scope>test</scope> so it is only used for tests and not bundled, and bump the
version to a maintained release (e.g., a 7.x TestNG) to avoid an ancient 2018
release; also verify the Maven Shade plugin or any
dependencyManagement/exclusions do not inadvertently include test-scoped
dependencies when building the shaded artifact.

<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.33.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.appium/java-client -->
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>9.4.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.0</version>
</dependency>

<!-- > Additional dependencies -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version> <!-- Make sure to use the latest version -->
</dependency>
Comment on lines +59 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Jackson version mismatch will cause runtime failures.

jackson-annotations is pinned to 2.13.0 while jackson-databind is 2.12.3. Jackson components must share the same minor version; mismatched versions are a documented source of JsonMappingException and annotation-processing failures at runtime.

🐛 Proposed fix: align to the same minor version
 <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-annotations</artifactId>
-    <version>2.13.0</version>
+    <version>2.13.5</version>
 </dependency>
 ...
 <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
-    <version>2.12.3</version>
+    <version>2.13.5</version>
 </dependency>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.0</version>
</dependency>
<!-- > Additional dependencies -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version> <!-- Make sure to use the latest version -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.5</version>
</dependency>
<!-- > Additional dependencies -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version> <!-- Make sure to use the latest version -->
</dependency>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@string_to_image_action/pom.xml` around lines 59 - 80, The pom declares
jackson-annotations at 2.13.0 but jackson-databind at 2.12.3 which will cause
runtime Jackson incompatibilities; update the pom so all Jackson artifacts
(e.g., jackson-annotations and jackson-databind) use the same minor version
(pick 2.13.0 or a single newer matching version) and verify any other Jackson
dependencies (groupId com.fasterxml.jackson.core) are aligned to that same
version to avoid JsonMappingException/annotation-processing failures.

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.4</version>
</dependency>

</dependencies>
<build>
<finalName>string_to_image_action</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven.source.plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.testsigma.addons.util;

import com.testsigma.sdk.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.EntityBuilder;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;

public class ImageComparisonUtils {
WebDriver driver;
Logger logger;

public ImageComparisonUtils(WebDriver driver, Logger logger) {
this.driver = driver;
this.logger = logger;
}

RequestConfig config = RequestConfig.custom()
.setSocketTimeout(10 * 60 * 1000)
.setConnectionRequestTimeout(60 * 1000)
.setConnectTimeout(60 * 1000)
.build();

public boolean uploadFile(String s3SignedURL, String localPath) {
logger.debug("s3SignedURL - " + s3SignedURL);
logger.debug("localPath - " + localPath);
boolean localUrlExists = new File(localPath).exists();
if (localUrlExists) {
logger.info(String.format("Uploading test asset to storage, presigned-URL:%s, localFilePath:%s", s3SignedURL, localPath));
try (CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(config).build()) {
HttpPut httpPut = new HttpPut(s3SignedURL);

File file = new File(localPath);
HttpEntity entity = EntityBuilder.create().setFile(file).build();
httpPut.setEntity(entity);
HttpResponse response = httpclient.execute(httpPut);
logger.info("Upload completed");
return true;
} catch (Exception e) {
logger.info("Exception while uploading custom screenshot to s3: " + ExceptionUtils.getStackTrace(e));
return false;
}
} else {
logger.info("Local path does not exist");
return false;
}
}
Comment on lines +41 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

S3 upload response status is never checked — silent false-positive success.

httpclient.execute(httpPut) returns an HttpResponse whose status code is discarded. If S3 responds with a 403 (e.g., expired presigned URL) or 400 (content-type mismatch), the method logs "Upload completed" and returns true, causing the caller to report SUCCESS when the image was never actually stored.

🐛 Proposed fix: inspect the response status code
 HttpResponse response = httpclient.execute(httpPut);
-logger.info("Upload completed");
-return true;
+int statusCode = response.getStatusLine().getStatusCode();
+logger.info("Upload completed with HTTP status: " + statusCode);
+if (statusCode >= 200 && statusCode < 300) {
+    return true;
+} else {
+    logger.info("Upload failed with unexpected HTTP status: " + statusCode);
+    return false;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public boolean uploadFile(String s3SignedURL, String localPath) {
logger.debug("s3SignedURL - " + s3SignedURL);
logger.debug("localPath - " + localPath);
boolean localUrlExists = new File(localPath).exists();
if (localUrlExists) {
logger.info(String.format("Uploading test asset to storage, presigned-URL:%s, localFilePath:%s", s3SignedURL, localPath));
try (CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(config).build()) {
HttpPut httpPut = new HttpPut(s3SignedURL);
File file = new File(localPath);
HttpEntity entity = EntityBuilder.create().setFile(file).build();
httpPut.setEntity(entity);
HttpResponse response = httpclient.execute(httpPut);
logger.info("Upload completed");
return true;
} catch (Exception e) {
logger.info("Exception while uploading custom screenshot to s3: " + ExceptionUtils.getStackTrace(e));
return false;
}
} else {
logger.info("Local path does not exist");
return false;
}
}
public boolean uploadFile(String s3SignedURL, String localPath) {
logger.debug("s3SignedURL - " + s3SignedURL);
logger.debug("localPath - " + localPath);
boolean localUrlExists = new File(localPath).exists();
if (localUrlExists) {
logger.info(String.format("Uploading test asset to storage, presigned-URL:%s, localFilePath:%s", s3SignedURL, localPath));
try (CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(config).build()) {
HttpPut httpPut = new HttpPut(s3SignedURL);
File file = new File(localPath);
HttpEntity entity = EntityBuilder.create().setFile(file).build();
httpPut.setEntity(entity);
HttpResponse response = httpclient.execute(httpPut);
int statusCode = response.getStatusLine().getStatusCode();
logger.info("Upload completed with HTTP status: " + statusCode);
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
logger.info("Upload failed with unexpected HTTP status: " + statusCode);
return false;
}
} catch (Exception e) {
logger.info("Exception while uploading custom screenshot to s3: " + ExceptionUtils.getStackTrace(e));
return false;
}
} else {
logger.info("Local path does not exist");
return false;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@string_to_image_action/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java`
around lines 41 - 64, The uploadFile method currently ignores the HttpResponse
from httpclient.execute(httpPut) and always returns true; update uploadFile to
inspect the HttpResponse status (response.getStatusLine().getStatusCode()) after
executing the HttpPut and only return true for successful 2xx codes, otherwise
log an error with the status code and any response body or reason phrase and
return false; ensure exception handling still returns false and logs the stack
trace via ExceptionUtils.getStackTrace(e).


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.testsigma.addons.util;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;

/**
* Converts a string into an image with white background and the text drawn on it.
* Font size is chosen based on the input text length (shorter text = larger font).
*/
public final class StringToImageConverter {

private static final int MIN_FONT_SIZE = 12;
private static final int MAX_FONT_SIZE = 72;
private static final int PADDING = 40;
private static final int MIN_IMAGE_WIDTH = 200;
private static final int MAX_IMAGE_WIDTH = 1200;
private static final String FONT_NAME = Font.SANS_SERIF;

private StringToImageConverter() {
}

/**
* Converts the given text into a BufferedImage with white background and black text.
* Font size is derived from text length.
*
* @param text the string to render (can be null or empty; empty yields a small placeholder image)
* @return BufferedImage with white background and text
*/
public static BufferedImage convert(String text) {
String safeText = text == null ? "" : text;
int fontSize = computeFontSize(safeText.length());
Font font = new Font(FONT_NAME, Font.PLAIN, fontSize);

FontMetrics fm = getFontMetrics(font);
List<String> lines = wrapLines(safeText, fm, MAX_IMAGE_WIDTH - 2 * PADDING);
int lineHeight = fm.getHeight();
int ascent = fm.getAscent();
int imageWidth = Math.max(MIN_IMAGE_WIDTH, computeTextWidth(lines, fm) + 2 * PADDING);
int imageHeight = Math.max(40, lines.size() * lineHeight + 2 * PADDING);

BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setColor(Color.WHITE);
g.fillRect(0, 0, imageWidth, imageHeight);
g.setColor(Color.BLACK);
g.setFont(font);

int y = PADDING + ascent;
for (String line : lines) {
g.drawString(line, PADDING, y);
y += lineHeight;
}
} finally {
g.dispose();
}
return image;
}

/**
* Converts the given text to an image and writes it to a temporary PNG file.
*
* @param text the string to render
* @return temporary File containing the PNG image
* @throws IOException if writing the file fails
*/
public static File convertToFile(String text) throws IOException {
BufferedImage image = convert(text);
File tempFile = File.createTempFile("testsigma-text-image-", ".png");
ImageIO.write(image, "png", tempFile);
return tempFile;
}

private static int computeFontSize(int textLength) {
if (textLength <= 0) return MAX_FONT_SIZE;
if (textLength <= 15) return MAX_FONT_SIZE;
if (textLength <= 50) return 48;
if (textLength <= 150) return 32;
if (textLength <= 400) return 24;
if (textLength <= 800) return 18;
return Math.max(MIN_FONT_SIZE, 14);
}

private static FontMetrics getFontMetrics(Font font) {
BufferedImage dummy = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = dummy.createGraphics();
g.setFont(font);
FontMetrics fm = g.getFontMetrics();
g.dispose();
return fm;
}

private static List<String> wrapLines(String text, FontMetrics fm, int maxWidth) {
List<String> lines = new ArrayList<>();
if (text.isEmpty()) {
lines.add(" ");
return lines;
}
String[] paragraphs = text.split("\\n", -1);
for (String para : paragraphs) {
if (para.isEmpty()) {
lines.add(" ");
continue;
}
StringBuilder line = new StringBuilder();
for (String word : para.split(" ", -1)) {
String candidate = line.length() == 0 ? word : line + " " + word;
if (fm.stringWidth(candidate) <= maxWidth) {
line.setLength(0);
line.append(candidate);
} else {
if (line.length() > 0) {
lines.add(line.toString());
line.setLength(0);
}
if (fm.stringWidth(word) <= maxWidth) {
line.append(word);
} else {
for (int i = 0; i < word.length(); ) {
int fit = 0;
int maxFit = word.length() - i;
while (fit < maxFit && fm.stringWidth(word.substring(i, i + fit + 1)) <= maxWidth) {
fit++;
}
if (fit == 0) fit = 1;
lines.add(word.substring(i, i + fit));
i += fit;
}
}
}
}
if (line.length() > 0) {
lines.add(line.toString());
}
}
return lines.isEmpty() ? List.of(" ") : lines;
}

private static int computeTextWidth(List<String> lines, FontMetrics fm) {
int max = 0;
for (String line : lines) {
max = Math.max(max, fm.stringWidth(line));
}
return max;
}
}
Loading