diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java
index 780b87751..4f4f2a72d 100644
--- a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java
+++ b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java
@@ -18,8 +18,13 @@
import java.util.*;
import java.util.regex.Pattern;
+import com.microsoft.playwright.Locator;
import com.microsoft.playwright.options.AriaRole;
+import com.microsoft.playwright.options.Clip;
import com.microsoft.playwright.options.PseudoElement;
+import com.microsoft.playwright.options.ScreenshotAnimations;
+import com.microsoft.playwright.options.ScreenshotCaret;
+import com.microsoft.playwright.options.ScreenshotScale;
/**
* The {@code LocatorAssertions} class provides assertion methods that can be used to make assertions about the {@code
@@ -59,6 +64,168 @@ public IsAttachedOptions setTimeout(double timeout) {
return this;
}
}
+ class HasScreenshotOptions {
+ /**
+ * When set to {@code "disabled"}, stops CSS animations, CSS transitions and Web Animations. Animations get different
+ * treatment depending on their duration:
+ *
+ * - finite animations are fast-forwarded to completion, so they'll fire {@code transitionend} event.
+ * - infinite animations are canceled to initial state, and then played over after the screenshot.
+ *
+ *
+ * Defaults to {@code "disabled"}.
+ */
+ public ScreenshotAnimations animations;
+ /**
+ * When set to {@code "hide"}, screenshot will hide text caret. When set to {@code "initial"}, text caret behavior will not
+ * be changed. Defaults to {@code "hide"}.
+ */
+ public ScreenshotCaret caret;
+ /**
+ * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ */
+ public List mask;
+ /**
+ * Specify the color of the overlay box for masked elements, in CSS color format. Default color is pink {@code
+ * #FF00FF}.
+ */
+ public String maskColor;
+ /**
+ * An acceptable amount of pixels that could be different. Unset by default.
+ */
+ public Integer maxDiffPixels;
+ /**
+ * An acceptable ratio of pixels that are different to the total amount of pixels, between {@code 0} and {@code 1}. Unset
+ * by default.
+ */
+ public Double maxDiffPixelRatio;
+ /**
+ * Hides default white background and allows capturing screenshots with transparency. Not applicable to {@code jpeg}
+ * images. Defaults to {@code false}.
+ */
+ public Boolean omitBackground;
+ /**
+ * When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices,
+ * this will keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so
+ * screenshots of high-dpi devices will be twice as large or even larger.
+ *
+ * Defaults to {@code "css"}.
+ */
+ public ScreenshotScale scale;
+ /**
+ * Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
+ * invisible or change their properties to help you creating repeatable screenshots.
+ */
+ public String style;
+ /**
+ * An acceptable perceived color difference between the same pixel in compared images, between zero (strict) and one
+ * (lax), default is {@code 0.2}.
+ */
+ public Double threshold;
+ /**
+ * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
+ */
+ public Double timeout;
+
+ /**
+ * When set to {@code "disabled"}, stops CSS animations, CSS transitions and Web Animations. Animations get different
+ * treatment depending on their duration:
+ *
+ * - finite animations are fast-forwarded to completion, so they'll fire {@code transitionend} event.
+ * - infinite animations are canceled to initial state, and then played over after the screenshot.
+ *
+ *
+ * Defaults to {@code "disabled"}.
+ */
+ public HasScreenshotOptions setAnimations(ScreenshotAnimations animations) {
+ this.animations = animations;
+ return this;
+ }
+ /**
+ * When set to {@code "hide"}, screenshot will hide text caret. When set to {@code "initial"}, text caret behavior will not
+ * be changed. Defaults to {@code "hide"}.
+ */
+ public HasScreenshotOptions setCaret(ScreenshotCaret caret) {
+ this.caret = caret;
+ return this;
+ }
+ /**
+ * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ */
+ public HasScreenshotOptions setMask(List mask) {
+ this.mask = mask;
+ return this;
+ }
+ /**
+ * Specify the color of the overlay box for masked elements, in CSS color format. Default color is pink {@code
+ * #FF00FF}.
+ */
+ public HasScreenshotOptions setMaskColor(String maskColor) {
+ this.maskColor = maskColor;
+ return this;
+ }
+ /**
+ * An acceptable amount of pixels that could be different. Unset by default.
+ */
+ public HasScreenshotOptions setMaxDiffPixels(int maxDiffPixels) {
+ this.maxDiffPixels = maxDiffPixels;
+ return this;
+ }
+ /**
+ * An acceptable ratio of pixels that are different to the total amount of pixels, between {@code 0} and {@code 1}. Unset
+ * by default.
+ */
+ public HasScreenshotOptions setMaxDiffPixelRatio(double maxDiffPixelRatio) {
+ this.maxDiffPixelRatio = maxDiffPixelRatio;
+ return this;
+ }
+ /**
+ * Hides default white background and allows capturing screenshots with transparency. Not applicable to {@code jpeg}
+ * images. Defaults to {@code false}.
+ */
+ public HasScreenshotOptions setOmitBackground(boolean omitBackground) {
+ this.omitBackground = omitBackground;
+ return this;
+ }
+ /**
+ * When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices,
+ * this will keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so
+ * screenshots of high-dpi devices will be twice as large or even larger.
+ *
+ * Defaults to {@code "css"}.
+ */
+ public HasScreenshotOptions setScale(ScreenshotScale scale) {
+ this.scale = scale;
+ return this;
+ }
+ /**
+ * Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
+ * invisible or change their properties to help you creating repeatable screenshots.
+ */
+ public HasScreenshotOptions setStyle(String style) {
+ this.style = style;
+ return this;
+ }
+ /**
+ * An acceptable perceived color difference between the same pixel in compared images, between zero (strict) and one
+ * (lax), default is {@code 0.2}.
+ */
+ public HasScreenshotOptions setThreshold(double threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+ /**
+ * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
+ */
+ public HasScreenshotOptions setTimeout(double timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+ }
class IsCheckedOptions {
/**
* Provides state to assert for. Asserts for input to be checked by default. This option can't be used when {@code
@@ -2459,5 +2626,78 @@ default void matchesAriaSnapshot(String expected) {
* @since v1.49
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
+ /**
+ * This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ *
Usage
+ *
{@code
+ * Locator locator = page.getByRole(AriaRole.BUTTON);
+ * assertThat(locator).hasScreenshot("image.png");
+ * }
+ *
+ * Note that screenshot assertions only work with the Playwright driver's screenshot comparison support; there is no
+ * concept of a test-runner-managed snapshot directory as in {@code @playwright/test}. By default, baseline images are
+ * stored under {@code src/test/resources/__screenshots__//}, overridable via the {@code
+ * playwright.snapshotDir} system property. Pass {@code -Dplaywright.updateSnapshots=true} to (re-)generate baselines.
+ *
+ * @param name Snapshot name. Must have a {@code .png} or {@code .webp} extension, the screenshot is captured in the corresponding format. Both formats are lossless.
+ * @since v1.23
+ */
+ default void hasScreenshot(String name) {
+ hasScreenshot(name, null);
+ }
+ /**
+ * This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * @param name Snapshot name. Must have a {@code .png} or {@code .webp} extension, the screenshot is captured in the corresponding format. Both formats are lossless.
+ * @since v1.23
+ */
+ void hasScreenshot(String name, HasScreenshotOptions options);
+ /**
+ * This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * @param nameSegments Snapshot name segments that will be joined to form the file name. Must have a {@code .png} or {@code .webp} extension on the last segment.
+ * @since v1.23
+ */
+ default void hasScreenshot(String[] nameSegments) {
+ hasScreenshot(nameSegments, null);
+ }
+ /**
+ * This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * @param nameSegments Snapshot name segments that will be joined to form the file name. Must have a {@code .png} or {@code .webp} extension on the last segment.
+ * @since v1.23
+ */
+ void hasScreenshot(String[] nameSegments, HasScreenshotOptions options);
+ /**
+ * This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * The snapshot is stored in the PNG format. To store it in the WebP format instead, pass a snapshot name with the
+ * {@code .webp} extension via {@link com.microsoft.playwright.assertions.LocatorAssertions#hasScreenshot
+ * LocatorAssertions.hasScreenshot()}.
+ *
+ *
Usage
+ *
{@code
+ * Locator locator = page.getByRole(AriaRole.BUTTON);
+ * assertThat(locator).hasScreenshot();
+ * }
+ *
+ * @since v1.23
+ */
+ default void hasScreenshot() {
+ hasScreenshot((HasScreenshotOptions) null);
+ }
+ /**
+ * This function will wait until two consecutive locator screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * @since v1.23
+ */
+ void hasScreenshot(HasScreenshotOptions options);
}
diff --git a/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java b/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java
index 88e529e38..9198749c0 100644
--- a/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java
+++ b/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java
@@ -16,7 +16,13 @@
package com.microsoft.playwright.assertions;
+import java.util.List;
import java.util.regex.Pattern;
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.options.Clip;
+import com.microsoft.playwright.options.ScreenshotAnimations;
+import com.microsoft.playwright.options.ScreenshotCaret;
+import com.microsoft.playwright.options.ScreenshotScale;
/**
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page}
@@ -51,6 +57,192 @@ public MatchesAriaSnapshotOptions setTimeout(double timeout) {
return this;
}
}
+ class HasScreenshotOptions {
+ /**
+ * When set to {@code "disabled"}, stops CSS animations, CSS transitions and Web Animations. Animations get different
+ * treatment depending on their duration:
+ *
+ * - finite animations are fast-forwarded to completion, so they'll fire {@code transitionend} event.
+ * - infinite animations are canceled to initial state, and then played over after the screenshot.
+ *
+ *
+ * Defaults to {@code "disabled"}.
+ */
+ public ScreenshotAnimations animations;
+ /**
+ * When set to {@code "hide"}, screenshot will hide text caret. When set to {@code "initial"}, text caret behavior will not
+ * be changed. Defaults to {@code "hide"}.
+ */
+ public ScreenshotCaret caret;
+ /**
+ * An object which specifies clipping of the resulting image.
+ */
+ public Clip clip;
+ /**
+ * When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
+ * {@code false}.
+ */
+ public Boolean fullPage;
+ /**
+ * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ */
+ public List mask;
+ /**
+ * Specify the color of the overlay box for masked elements, in CSS color format. Default color is pink {@code
+ * #FF00FF}.
+ */
+ public String maskColor;
+ /**
+ * An acceptable amount of pixels that could be different. Unset by default.
+ */
+ public Integer maxDiffPixels;
+ /**
+ * An acceptable ratio of pixels that are different to the total amount of pixels, between {@code 0} and {@code 1}. Unset
+ * by default.
+ */
+ public Double maxDiffPixelRatio;
+ /**
+ * Hides default white background and allows capturing screenshots with transparency. Not applicable to {@code jpeg}
+ * images. Defaults to {@code false}.
+ */
+ public Boolean omitBackground;
+ /**
+ * When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices,
+ * this will keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so
+ * screenshots of high-dpi devices will be twice as large or even larger.
+ *
+ * Defaults to {@code "css"}.
+ */
+ public ScreenshotScale scale;
+ /**
+ * Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
+ * invisible or change their properties to help you creating repeatable screenshots.
+ */
+ public String style;
+ /**
+ * An acceptable perceived color difference between the same pixel in compared images, between zero (strict) and one
+ * (lax), default is {@code 0.2}.
+ */
+ public Double threshold;
+ /**
+ * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
+ */
+ public Double timeout;
+
+ /**
+ * When set to {@code "disabled"}, stops CSS animations, CSS transitions and Web Animations. Animations get different
+ * treatment depending on their duration:
+ *
+ * - finite animations are fast-forwarded to completion, so they'll fire {@code transitionend} event.
+ * - infinite animations are canceled to initial state, and then played over after the screenshot.
+ *
+ *
+ * Defaults to {@code "disabled"}.
+ */
+ public HasScreenshotOptions setAnimations(ScreenshotAnimations animations) {
+ this.animations = animations;
+ return this;
+ }
+ /**
+ * When set to {@code "hide"}, screenshot will hide text caret. When set to {@code "initial"}, text caret behavior will not
+ * be changed. Defaults to {@code "hide"}.
+ */
+ public HasScreenshotOptions setCaret(ScreenshotCaret caret) {
+ this.caret = caret;
+ return this;
+ }
+ /**
+ * An object which specifies clipping of the resulting image.
+ */
+ public HasScreenshotOptions setClip(Clip clip) {
+ this.clip = clip;
+ return this;
+ }
+ /**
+ * When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
+ * {@code false}.
+ */
+ public HasScreenshotOptions setFullPage(boolean fullPage) {
+ this.fullPage = fullPage;
+ return this;
+ }
+ /**
+ * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box
+ * {@code #FF00FF} (customized by {@code maskColor}) that completely covers its bounding box.
+ */
+ public HasScreenshotOptions setMask(List mask) {
+ this.mask = mask;
+ return this;
+ }
+ /**
+ * Specify the color of the overlay box for masked elements, in CSS color format. Default color is pink {@code
+ * #FF00FF}.
+ */
+ public HasScreenshotOptions setMaskColor(String maskColor) {
+ this.maskColor = maskColor;
+ return this;
+ }
+ /**
+ * An acceptable amount of pixels that could be different. Unset by default.
+ */
+ public HasScreenshotOptions setMaxDiffPixels(int maxDiffPixels) {
+ this.maxDiffPixels = maxDiffPixels;
+ return this;
+ }
+ /**
+ * An acceptable ratio of pixels that are different to the total amount of pixels, between {@code 0} and {@code 1}. Unset
+ * by default.
+ */
+ public HasScreenshotOptions setMaxDiffPixelRatio(double maxDiffPixelRatio) {
+ this.maxDiffPixelRatio = maxDiffPixelRatio;
+ return this;
+ }
+ /**
+ * Hides default white background and allows capturing screenshots with transparency. Not applicable to {@code jpeg}
+ * images. Defaults to {@code false}.
+ */
+ public HasScreenshotOptions setOmitBackground(boolean omitBackground) {
+ this.omitBackground = omitBackground;
+ return this;
+ }
+ /**
+ * When set to {@code "css"}, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices,
+ * this will keep screenshots small. Using {@code "device"} option will produce a single pixel per each device pixel, so
+ * screenshots of high-dpi devices will be twice as large or even larger.
+ *
+ * Defaults to {@code "css"}.
+ */
+ public HasScreenshotOptions setScale(ScreenshotScale scale) {
+ this.scale = scale;
+ return this;
+ }
+ /**
+ * Text of the stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
+ * invisible or change their properties to help you creating repeatable screenshots.
+ */
+ public HasScreenshotOptions setStyle(String style) {
+ this.style = style;
+ return this;
+ }
+ /**
+ * An acceptable perceived color difference between the same pixel in compared images, between zero (strict) and one
+ * (lax), default is {@code 0.2}.
+ */
+ public HasScreenshotOptions setThreshold(double threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+ /**
+ * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
+ */
+ public HasScreenshotOptions setTimeout(double timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+ }
class HasTitleOptions {
/**
* Time to retry the assertion for in milliseconds. Defaults to {@code 5000}.
@@ -139,6 +331,87 @@ default void matchesAriaSnapshot(String expected) {
* @since v1.60
*/
void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions options);
+ /**
+ * This function will wait until two consecutive page screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ *
Usage
+ *
{@code
+ * assertThat(page).hasScreenshot("image.png");
+ * }
+ *
+ * Note that screenshot assertions only work with the Playwright driver's screenshot comparison support; there is no
+ * concept of a test-runner-managed snapshot directory as in {@code @playwright/test}. By default, baseline images are
+ * stored under {@code src/test/resources/__screenshots__//}, overridable via the {@code
+ * playwright.snapshotDir} system property. Pass {@code -Dplaywright.updateSnapshots=true} to (re-)generate baselines.
+ *
+ * @param name Snapshot name. Must have a {@code .png} or {@code .webp} extension, the screenshot is captured in the corresponding format. Both formats are lossless.
+ * @since v1.23
+ */
+ default void hasScreenshot(String name) {
+ hasScreenshot(name, null);
+ }
+ /**
+ * This function will wait until two consecutive page screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * Usage
+ *
{@code
+ * assertThat(page).hasScreenshot("image.png");
+ * }
+ *
+ * @param name Snapshot name. Must have a {@code .png} or {@code .webp} extension, the screenshot is captured in the corresponding format. Both formats are lossless.
+ * @since v1.23
+ */
+ void hasScreenshot(String name, HasScreenshotOptions options);
+ /**
+ * This function will wait until two consecutive page screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * Usage
+ *
{@code
+ * assertThat(page).hasScreenshot(new String[] {"folder", "image.png"});
+ * }
+ *
+ * @param nameSegments Snapshot name segments that will be joined to form the file name. Must have a {@code .png} or {@code .webp} extension on the last segment.
+ * @since v1.23
+ */
+ default void hasScreenshot(String[] nameSegments) {
+ hasScreenshot(nameSegments, null);
+ }
+ /**
+ * This function will wait until two consecutive page screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * @param nameSegments Snapshot name segments that will be joined to form the file name. Must have a {@code .png} or {@code .webp} extension on the last segment.
+ * @since v1.23
+ */
+ void hasScreenshot(String[] nameSegments, HasScreenshotOptions options);
+ /**
+ * This function will wait until two consecutive page screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * The snapshot is stored in the PNG format. To store it in the WebP format instead, pass a snapshot name with the
+ * {@code .webp} extension via {@link com.microsoft.playwright.assertions.PageAssertions#hasScreenshot
+ * PageAssertions.hasScreenshot()}.
+ *
+ *
Usage
+ *
{@code
+ * assertThat(page).hasScreenshot();
+ * }
+ *
+ * @since v1.23
+ */
+ default void hasScreenshot() {
+ hasScreenshot((HasScreenshotOptions) null);
+ }
+ /**
+ * This function will wait until two consecutive page screenshots yield the same result, and then compare the last
+ * screenshot with the expectation.
+ *
+ * @since v1.23
+ */
+ void hasScreenshot(HasScreenshotOptions options);
/**
* Ensures the page has the given title.
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java
index 088910c98..b7dc9d083 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorAssertionsImpl.java
@@ -385,6 +385,26 @@ public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snap
expectImpl("to.match.aria", options, expected,"Locator expected to match Aria snapshot", "Assert \"matchesAriaSnapshot\"");
}
+ @Override
+ public void hasScreenshot(String name, HasScreenshotOptions options) {
+ hasScreenshotImpl(name, options);
+ }
+
+ @Override
+ public void hasScreenshot(String[] nameSegments, HasScreenshotOptions options) {
+ hasScreenshotImpl(nameSegments, options);
+ }
+
+ @Override
+ public void hasScreenshot(HasScreenshotOptions options) {
+ hasScreenshotImpl(null, options);
+ }
+
+ private void hasScreenshotImpl(Object nameOrNames, HasScreenshotOptions options) {
+ ScreenshotAssertionsOptions screenshotOptions = convertType(options, ScreenshotAssertionsOptions.class);
+ new ScreenshotAssertionsHelper(actualLocator.frame.page, actualLocator, isNot).assertScreenshot(nameOrNames, screenshotOptions, "Assert \"hasScreenshot\"");
+ }
+
@Override
public void isChecked(IsCheckedOptions options) {
if (options == null) {
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java
index 2cdd63d17..4f130a4e4 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageAssertionsImpl.java
@@ -83,6 +83,26 @@ public void matchesAriaSnapshot(String expected, MatchesAriaSnapshotOptions snap
expectImpl("to.match.aria", options, expected, "Page expected to match Aria snapshot", "Assert \"matchesAriaSnapshot\"");
}
+ @Override
+ public void hasScreenshot(String name, HasScreenshotOptions options) {
+ hasScreenshotImpl(name, options);
+ }
+
+ @Override
+ public void hasScreenshot(String[] nameSegments, HasScreenshotOptions options) {
+ hasScreenshotImpl(nameSegments, options);
+ }
+
+ @Override
+ public void hasScreenshot(HasScreenshotOptions options) {
+ hasScreenshotImpl(null, options);
+ }
+
+ private void hasScreenshotImpl(Object nameOrNames, HasScreenshotOptions options) {
+ ScreenshotAssertionsOptions screenshotOptions = convertType(options, ScreenshotAssertionsOptions.class);
+ new ScreenshotAssertionsHelper(actualPage, null, isNot).assertScreenshot(nameOrNames, screenshotOptions, "Assert \"hasScreenshot\"");
+ }
+
@Override
public PageAssertions not() {
return new PageAssertionsImpl(actualPage, !isNot);
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java
index 4956ea623..fe2e2a7d8 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java
@@ -1246,6 +1246,45 @@ public List selectOption(String selector, SelectOption value, SelectOpti
return selectOption(selector, values, options);
}
+ static class ExpectScreenshotResult {
+ byte[] actual;
+ byte[] previous;
+ byte[] diff;
+ String errorMessage;
+ List log;
+ boolean timedOut;
+ }
+
+ ExpectScreenshotResult expectScreenshot(PageExpectScreenshotOptions options, String title) {
+ return withTitle(title, () -> expectScreenshot(options));
+ }
+
+ ExpectScreenshotResult expectScreenshot(PageExpectScreenshotOptions options) {
+ JsonObject params = gson().toJsonTree(options).getAsJsonObject();
+ ExpectScreenshotResult result = new ExpectScreenshotResult();
+ try {
+ JsonObject json = sendMessage("expectScreenshot", params, options.timeout).getAsJsonObject();
+ if (json.has("actual")) {
+ result.actual = Base64.getDecoder().decode(json.get("actual").getAsString());
+ }
+ } catch (ServerErrorWithDetails e) {
+ PageExpectScreenshotErrorDetails details = gson().fromJson(e.errorDetails(), PageExpectScreenshotErrorDetails.class);
+ if (details.actual != null) {
+ result.actual = Base64.getDecoder().decode(details.actual);
+ }
+ if (details.previous != null) {
+ result.previous = Base64.getDecoder().decode(details.previous);
+ }
+ if (details.diff != null) {
+ result.diff = Base64.getDecoder().decode(details.diff);
+ }
+ result.errorMessage = details.customErrorMessage;
+ result.log = details.log;
+ result.timedOut = Boolean.TRUE.equals(details.timedOut);
+ }
+ return result;
+ }
+
private byte[] screenshotImpl(ScreenshotOptions options) {
if (options == null) {
options = new ScreenshotOptions();
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java
index b7e02eed2..ebc27f884 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java
@@ -128,4 +128,40 @@ class FrameExpectErrorDetails {
String customErrorMessage;
}
+class PageExpectScreenshotOptions {
+ String expected;
+ boolean isNot;
+ LocatorImpl locator;
+ String comparator;
+ Double maxDiffPixels;
+ Double maxDiffPixelRatio;
+ Double threshold;
+ Boolean fullPage;
+ com.microsoft.playwright.options.Clip clip;
+ // Only "png" or "webp" are valid here, computed internally based on the snapshot
+ // file extension (there is no public "webp" value in ScreenshotType).
+ String type;
+ Boolean omitBackground;
+ com.microsoft.playwright.options.ScreenshotCaret caret;
+ com.microsoft.playwright.options.ScreenshotAnimations animations;
+ com.microsoft.playwright.options.ScreenshotScale scale;
+ List mask;
+ String maskColor;
+ String style;
+ Double timeout;
+}
+
+class PageExpectScreenshotResult {
+ String actual;
+}
+
+class PageExpectScreenshotErrorDetails {
+ String diff;
+ String customErrorMessage;
+ String actual;
+ String previous;
+ Boolean timedOut;
+ List log;
+}
+
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsHelper.java b/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsHelper.java
new file mode 100644
index 000000000..8560ca3be
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsHelper.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright.impl;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.PlaywrightException;
+import com.microsoft.playwright.options.ScreenshotAnimations;
+import com.microsoft.playwright.options.ScreenshotCaret;
+import com.microsoft.playwright.options.ScreenshotScale;
+import org.opentest4j.AssertionFailedError;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+// Implements the Java counterpart of `toHaveScreenshot()`, ported from the
+// `SnapshotHelper` logic in `packages/playwright/src/matchers/toMatchSnapshot.ts`
+// of the upstream Playwright repository. Since the Java bindings do not have a
+// dedicated test-runner (unlike @playwright/test), the snapshot storage/update
+// conventions below are a Java-specific adaptation:
+// - Snapshots are stored under "src/test/resources/__screenshots__//"
+// by default. Override the root with the "playwright.snapshotDir" system property.
+// - Pass "-Dplaywright.updateSnapshots=true" to (re-)generate/update baseline snapshots,
+// analogous to "--update-snapshots" for @playwright/test.
+// The actual pixel-level image comparison (pixelmatch/SSIM) is performed by the
+// Playwright driver (Node.js) via the "Page.expectScreenshot" protocol method -
+// this class only handles the Java-side snapshot lifecycle (resolving the file,
+// reading/writing baselines) and error formatting.
+class ScreenshotAssertionsHelper {
+ private static final String SNAPSHOT_DIR_PROPERTY = "playwright.snapshotDir";
+ private static final String DEFAULT_SNAPSHOT_DIR = "src/test/resources/__screenshots__";
+ private static final String UPDATE_SNAPSHOTS_PROPERTY = "playwright.updateSnapshots";
+ private static final ConcurrentHashMap anonymousNameCounters = new ConcurrentHashMap<>();
+
+ private final PageImpl page;
+ private final LocatorImpl locator;
+ private final boolean isNot;
+
+ ScreenshotAssertionsHelper(PageImpl page, LocatorImpl locator, boolean isNot) {
+ this.page = page;
+ this.locator = locator;
+ this.isNot = isNot;
+ }
+
+ void assertScreenshot(Object nameOrNames, ScreenshotAssertionsOptions options, String title) {
+ if (options == null) {
+ options = new ScreenshotAssertionsOptions();
+ }
+ Path expectedPath = resolveSnapshotPath(nameOrNames);
+ String extension = expectedPath.getFileName().toString().toLowerCase().endsWith(".webp") ? "webp" : "png";
+
+ PageExpectScreenshotOptions protocolOptions = toProtocolOptions(options, extension);
+ protocolOptions.timeout = options.timeout == null ? AssertionsTimeout.defaultTimeout : options.timeout;
+ protocolOptions.isNot = isNot;
+
+ boolean hasSnapshot = Files.exists(expectedPath);
+
+ if (isNot) {
+ if (!hasSnapshot) {
+ // Nothing to compare against - matchers using ".not()" won't write baselines automatically.
+ return;
+ }
+ protocolOptions.expected = encode(readFile(expectedPath));
+ PageImpl.ExpectScreenshotResult result = page.expectScreenshot(protocolOptions, title);
+ if (result.errorMessage == null) {
+ // Screenshots differ, exactly as ".not()" expects.
+ return;
+ }
+ throw new AssertionFailedError(title + "\nScreenshot comparison failed:\n Expected result should be different from the actual one." + callLog(result.log));
+ }
+
+ boolean updateAll = isUpdateSnapshotsAll();
+
+ if (!hasSnapshot) {
+ protocolOptions.expected = null;
+ PageImpl.ExpectScreenshotResult result = page.expectScreenshot(protocolOptions, title);
+ if (result.errorMessage != null) {
+ writeDebugArtifacts(expectedPath, result);
+ throw new AssertionFailedError(title + "\n" + result.errorMessage + callLog(result.log));
+ }
+ Utils.writeToFile(result.actual, expectedPath);
+ return;
+ }
+
+ byte[] expectedBytes = readFile(expectedPath);
+ if (updateAll) {
+ protocolOptions.expected = null;
+ PageImpl.ExpectScreenshotResult result = page.expectScreenshot(protocolOptions, title);
+ if (result.errorMessage != null) {
+ writeDebugArtifacts(expectedPath, result);
+ throw new AssertionFailedError(title + "\n Failed to re-generate expected.\n" + result.errorMessage + callLog(result.log));
+ }
+ if (!Arrays.equals(result.actual, expectedBytes)) {
+ Utils.writeToFile(result.actual, expectedPath);
+ }
+ return;
+ }
+
+ protocolOptions.expected = encode(expectedBytes);
+ PageImpl.ExpectScreenshotResult result = page.expectScreenshot(protocolOptions, title);
+ if (result.errorMessage == null) {
+ return;
+ }
+ writeDebugArtifacts(expectedPath, result);
+ throw new AssertionFailedError(title + "\nScreenshot comparison failed:\n " + result.errorMessage +
+ callLog(result.log) + "\n\n Expected: " + expectedPath +
+ "\n Actual: " + actualDebugPath(expectedPath) +
+ (result.diff != null ? "\n Diff: " + diffDebugPath(expectedPath) : ""));
+ }
+
+ private PageExpectScreenshotOptions toProtocolOptions(ScreenshotAssertionsOptions options, String extension) {
+ PageExpectScreenshotOptions result = new PageExpectScreenshotOptions();
+ result.locator = locator;
+ result.animations = options.animations == null ? ScreenshotAnimations.DISABLED : options.animations;
+ result.caret = options.caret == null ? ScreenshotCaret.HIDE : options.caret;
+ result.clip = options.clip;
+ result.fullPage = options.fullPage;
+ result.omitBackground = options.omitBackground;
+ result.scale = options.scale == null ? ScreenshotScale.CSS : options.scale;
+ result.maxDiffPixels = options.maxDiffPixels == null ? null : options.maxDiffPixels.doubleValue();
+ result.maxDiffPixelRatio = options.maxDiffPixelRatio;
+ result.threshold = options.threshold;
+ result.maskColor = options.maskColor;
+ result.style = options.style;
+ result.type = extension;
+ if (options.mask != null) {
+ List mask = new ArrayList<>();
+ for (Locator l : options.mask) {
+ mask.add((LocatorImpl) l);
+ }
+ result.mask = mask;
+ }
+ return result;
+ }
+
+ private static boolean isUpdateSnapshotsAll() {
+ return Boolean.parseBoolean(System.getProperty(UPDATE_SNAPSHOTS_PROPERTY, "false"));
+ }
+
+ private static Path resolveSnapshotPath(Object nameOrNames) {
+ String name;
+ if (nameOrNames instanceof String[]) {
+ String[] segments = (String[]) nameOrNames;
+ name = String.join("-", segments);
+ } else if (nameOrNames instanceof String) {
+ name = (String) nameOrNames;
+ } else {
+ name = null;
+ }
+ StackTraceElement caller = callerFrame();
+ if (name == null || name.isEmpty()) {
+ String key = caller.getClassName() + "#" + caller.getMethodName();
+ int index = anonymousNameCounters.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
+ name = caller.getMethodName() + "-" + index + ".png";
+ }
+ if (!name.toLowerCase().endsWith(".png") && !name.toLowerCase().endsWith(".webp")) {
+ throw new PlaywrightException("Screenshot name \"" + name + "\" must have a '.png' or '.webp' extension");
+ }
+ String baseDir = System.getProperty(SNAPSHOT_DIR_PROPERTY, DEFAULT_SNAPSHOT_DIR);
+ String simpleClassName = simpleClassName(caller.getClassName());
+ return Paths.get(baseDir, simpleClassName, name);
+ }
+
+ private static String simpleClassName(String className) {
+ // Strip package name and any enclosing-class '$' qualifiers (e.g. anonymous/nested classes).
+ int dot = className.lastIndexOf('.');
+ String simple = dot == -1 ? className : className.substring(dot + 1);
+ int dollar = simple.indexOf('$');
+ return dollar == -1 ? simple : simple.substring(0, dollar);
+ }
+
+ private static StackTraceElement callerFrame() {
+ for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
+ String className = frame.getClassName();
+ if (className.startsWith("java.") || className.startsWith("jdk.")) {
+ continue;
+ }
+ if (className.startsWith("com.microsoft.playwright.impl.") || className.startsWith("com.microsoft.playwright.assertions.")) {
+ continue;
+ }
+ return frame;
+ }
+ throw new PlaywrightException("Could not determine the caller of the screenshot assertion to infer the snapshot name");
+ }
+
+ private static byte[] readFile(Path path) {
+ try {
+ return Files.readAllBytes(path);
+ } catch (IOException e) {
+ throw new PlaywrightException("Failed to read snapshot file: " + path, e);
+ }
+ }
+
+ private static String encode(byte[] bytes) {
+ return Base64.getEncoder().encodeToString(bytes);
+ }
+
+ private static Path actualDebugPath(Path expectedPath) {
+ return withSuffix(expectedPath, "-actual");
+ }
+
+ private static Path diffDebugPath(Path expectedPath) {
+ return withSuffix(expectedPath, "-diff");
+ }
+
+ private static Path withSuffix(Path expectedPath, String suffix) {
+ String fileName = expectedPath.getFileName().toString();
+ int dot = fileName.lastIndexOf('.');
+ String newFileName = dot == -1 ? fileName + suffix : fileName.substring(0, dot) + suffix + fileName.substring(dot);
+ Path parent = expectedPath.getParent();
+ return parent == null ? Paths.get(newFileName) : parent.resolve(newFileName);
+ }
+
+ private static void writeDebugArtifacts(Path expectedPath, PageImpl.ExpectScreenshotResult result) {
+ if (result.actual != null) {
+ Utils.writeToFile(result.actual, actualDebugPath(expectedPath));
+ }
+ if (result.diff != null) {
+ Utils.writeToFile(result.diff, diffDebugPath(expectedPath));
+ }
+ }
+
+ private static String callLog(List log) {
+ if (log == null || log.isEmpty()) {
+ return "";
+ }
+ return "\nCall log:\n" + String.join("\n", log);
+ }
+}
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsOptions.java b/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsOptions.java
new file mode 100644
index 000000000..9b90f876f
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsOptions.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright.impl;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.options.Clip;
+import com.microsoft.playwright.options.ScreenshotAnimations;
+import com.microsoft.playwright.options.ScreenshotCaret;
+import com.microsoft.playwright.options.ScreenshotScale;
+
+import java.util.List;
+
+// Common shape shared by PageAssertions.HasScreenshotOptions and LocatorAssertions.HasScreenshotOptions,
+// used as an intermediate type for Utils#convertType().
+class ScreenshotAssertionsOptions {
+ Double timeout;
+ ScreenshotAnimations animations;
+ ScreenshotCaret caret;
+ Clip clip;
+ Boolean fullPage;
+ List mask;
+ String maskColor;
+ Boolean omitBackground;
+ ScreenshotScale scale;
+ Integer maxDiffPixels;
+ Double maxDiffPixelRatio;
+ Double threshold;
+ String style;
+}
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestScreenshotAssertions.java b/playwright/src/test/java/com/microsoft/playwright/TestScreenshotAssertions.java
new file mode 100644
index 000000000..53ccba8a5
--- /dev/null
+++ b/playwright/src/test/java/com/microsoft/playwright/TestScreenshotAssertions.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.opentest4j.AssertionFailedError;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestScreenshotAssertions extends TestBase {
+ private static final Path SNAPSHOT_ROOT = Paths.get("src/test/resources/__screenshots__/TestScreenshotAssertions");
+
+ @BeforeEach
+ @AfterEach
+ void cleanupSnapshots() throws IOException {
+ if (Files.exists(SNAPSHOT_ROOT)) {
+ Files.walk(SNAPSHOT_ROOT)
+ .sorted((a, b) -> b.compareTo(a))
+ .forEach(p -> {
+ try {
+ Files.deleteIfExists(p);
+ } catch (IOException e) {
+ // ignore
+ }
+ });
+ }
+ }
+
+ @Test
+ void shouldGenerateAndMatchPageScreenshot() {
+ page.setContent("");
+ // First run: baseline does not exist yet, it should be created and the assertion should pass.
+ assertThat(page).hasScreenshot("page-baseline.png");
+ Path expected = SNAPSHOT_ROOT.resolve("page-baseline.png");
+ assertTrue(Files.exists(expected), "Baseline screenshot should have been created at " + expected);
+
+ // Second run: baseline exists and matches, assertion should pass without changes.
+ assertThat(page).hasScreenshot("page-baseline.png");
+ }
+
+ @Test
+ void shouldFailWhenScreenshotDiffers() throws IOException {
+ Path expected = SNAPSHOT_ROOT.resolve("page-mismatch.png");
+ page.setContent("");
+ assertThat(page).hasScreenshot("page-mismatch.png");
+ assertTrue(Files.exists(expected));
+
+ page.setContent("");
+ AssertionFailedError e = assertThrows(AssertionFailedError.class, () ->
+ assertThat(page).hasScreenshot("page-mismatch.png",
+ new com.microsoft.playwright.assertions.PageAssertions.HasScreenshotOptions().setTimeout(2_000)));
+ assertTrue(e.getMessage().contains("Screenshot comparison failed"), e.getMessage());
+ }
+
+ @Test
+ void shouldSupportNotWhenBaselineMissing() {
+ page.setContent("Hello
");
+ // No baseline exists - `.not()` should pass without writing a baseline.
+ assertThat(page).not().hasScreenshot("page-not-missing.png");
+ assertTrue(!Files.exists(SNAPSHOT_ROOT.resolve("page-not-missing.png")));
+ }
+
+ @Test
+ void shouldSupportLocatorScreenshot() {
+ page.setContent("");
+ Locator locator = page.locator("#box");
+ assertThat(locator).hasScreenshot("locator-baseline.png");
+ assertTrue(Files.exists(SNAPSHOT_ROOT.resolve("locator-baseline.png")));
+ }
+}