Skip to main content

Visual testing

io.github.shafthq:shaft-visual supplies the optional VisualProcessingProvider implementation and its OpenCV, Applitools Eyes, and Selenium Shutterbug dependencies.

Add the module

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.shafthq</groupId>
<artifactId>shaft-bom</artifactId>
<version>${shaft.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.github.shafthq</groupId>
<artifactId>shaft-engine</artifactId>
</dependency>
<dependency>
<groupId>io.github.shafthq</groupId>
<artifactId>shaft-visual</artifactId>
</dependency>
</dependencies>

No initialization call is required. Java ServiceLoader discovers the provider.

Dependency decision

Requires shaft-visual

APIFunctionality
matchesReferenceImage()Reference-image comparison. WebDriver/Appium default to Shutterbug; Playwright uses OpenCV screenshot-byte comparison.
matchesReferenceImage(VisualValidationEngine)OpenCV, Shutterbug, or Eyes comparison selected by the enum. Playwright routes Locator.screenshot() bytes through the provider; Shutterbug requests fall back to OpenCV because Shutterbug is Selenium-backed.
doesNotMatchReferenceImage() and its overloadNegative OpenCV/visual-engine comparison.
TouchActions.tap(String)Finds and taps an image inside the current screen.
TouchActions.type(String, ...)Finds an image, taps it, then types into the active field.
TouchActions.waitUntilElementIsVisible(String)Waits for an image match.
TouchActions.waitUntilElementIsNotVisible(String)Waits until an image match disappears from the screen.
TouchActions.swipeElementIntoView(String, ...)Swipes until the reference image is found.
ImageProcessingActions.findImageWithinCurrentPage(...)Direct OpenCV-backed image lookup.
ImageProcessingActions.compareAgainstBaseline(...)Direct baseline comparison.
ImageProcessingActions.loadOpenCV()Explicit provider/native-library loading.
Built-in Cucumber OpenCV, Shutterbug, and Eyes assertion stepsDelegates to the same provider.

Image-path touch actions compare against viewport screenshots and return viewport-relative coordinates for Selenium/Appium pointer actions. OpenCV matching is scale-tolerant, so a cropped reference image can be captured at a different DPI or display scale than the current app screenshot.

The bundled TestNG/JUnit web samples use:

driver.browser().navigateToURL(targetUrl)
.and().element().assertThat(logo).matchesReferenceImage();

The bundled Cucumber sample uses:

Then I Assert that the element found by "xpath": "//div[contains(@class,'container_fullWidth__1H_L8')]//img", exactly matches with the expected reference image using AI OpenCV

Both styles require shaft-visual.

SHAFT.GUI.Playwright supports the same element visual assertion surface. The Playwright backend captures Locator.screenshot() bytes and compares them through shaft-visual:

PlaywrightVisualValidation.java
driver.assertThat().element(By.id("logo"))
.matchesReferenceImage(ValidationEnums.VisualValidationEngine.EXACT_OPENCV);

The no-argument Playwright overload uses EXACT_OPENCV. Applitools Eyes engines also receive Playwright screenshot bytes. Selenium Shutterbug remains available for WebDriver/Appium visual checks.

Remains in shaft-engine

API/functionalityImplementation
WebDriver/Appium screenshots and report attachmentsSelenium/Appium plus SHAFT reporting.
ImageProcessingActions.highlightElementInScreenshot(...)JDK BufferedImage/Graphics2D.
ImageProcessingActions.compareImageFolders(...)JDK ImageIO and data buffers.
formatElementLocatorToImagePath(...)Baseline naming only.
getReferenceImage(...) and getShutterbugDifferencesImage(...)Baseline file reads only.
Animated GIF generationCore image/reporting implementation.
Locator-based touch methods such as tap(By)Selenium/Appium locator execution.
HealeniumIndependent integration.

Without shaft-visual, provider-dependent methods throw an IllegalStateException that names the missing Maven coordinate. Core screenshot and image-file operations continue to work.

Comparison engines

shaft-visual supports five visual validation engines through the VisualValidationEngine enum:

EngineDescriptionBest for
EXACT_EYESPixel-perfect comparisonStatic assets, logos, icons
STRICT_EYESHigh-sensitivity comparison with minor toleranceUI components
CONTENT_EYESCompares content while ignoring minor rendering differencesText-heavy pages
LAYOUT_EYESCompares layout structure, ignores content changesPage layout regression
OPENCVUses OpenCV for flexible image matchingComplex scenarios, partial matching
VisualTesting.java
import com.shaft.enums.internal.VisualValidationEngine;

// Assert element matches a reference image (stores baseline on first run)
driver.element().assertThat(By.id("logo")).matchesReferenceImage();

// Layout comparison — ignores content, checks structure
driver.element().assertThat(By.id("productCard"))
.matchesReferenceImage(VisualValidationEngine.LAYOUT_EYES);

When an intentional UI change is made, delete the relevant baseline image from src/test/resources/DynamicObjectRepository/ and run the test once to regenerate it. Run visual tests in a consistent environment (same OS, browser version, screen resolution) to avoid false positives, and avoid mixing headless and headed baselines.

matchesScreenshot()

matchesScreenshot() is a lighter-weight, OpenCV-only pixel-diff assertion built into shaft-engine (no shaft-visual dependency required). Like every other SHAFT assertion it runs immediately — no perform() is needed. Pass a VisualComparisonOptions object to tune the diff budget and masks, mirroring Playwright's toHaveScreenshot() options:

ScreenshotBaseline.java
driver.element().assertThat(By.id("logo"))
.matchesScreenshot(VisualComparisonOptions.create()
.maxDiffPixelRatio(0.01)
.mask(By.id("timestamp")));

For a straight comparison with default settings, call matchesScreenshot() with no arguments.

Per-browser/OS baseline naming

Baselines are stored per browser and platform, with a sanitized _<browser>_<platform> suffix appended to the hashed baseline file name (for example, <hash>_chrome_windows.png). The suffix is built from SHAFT.Properties.web.targetBrowserName() and SHAFT.Properties.platform.targetPlatform(), lowercased with non-alphanumeric characters stripped, so cross-browser and cross-OS runs no longer share (and fight over) a single baseline image.

If no per-browser/OS baseline exists yet, SHAFT falls back to a legacy unsuffixed baseline when one is present, logging a one-line notice, so baselines captured before this change keep working. New baselines — and any run with -Dshaft.updateSnapshots=true — always write to the new per-browser/OS path.

The IntelliJ plugin's Visual Baselines panel lists pending *_diff.png comparisons and lets you Accept or Reject each one without leaving the IDE — see the IntelliJ IDEA plugin guide.