From b0237f8ec2039f33899312e25388c5956a28d2f6 Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:13 +0530 Subject: [PATCH 1/9] feat: add visual comparison assertions --- .../assertions/LocatorAssertions.java | 386 ++++++++++++++---- 1 file changed, 299 insertions(+), 87 deletions(-) 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 6b79120b8..a8210ffad 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/LocatorAssertions.java @@ -16,11 +16,14 @@ package com.microsoft.playwright.assertions; -import org.jspecify.annotations.Nullable; import java.util.*; import java.util.regex.Pattern; +import com.microsoft.playwright.Locator; import com.microsoft.playwright.options.AriaRole; 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 @@ -42,11 +45,11 @@ */ public interface LocatorAssertions { class IsAttachedOptions { - public @Nullable Boolean attached; + public Boolean attached; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; public IsAttachedOptions setAttached(boolean attached) { this.attached = attached; @@ -60,21 +63,183 @@ 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: + * + * + *

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: + *

+ * + *

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 * indeterminate} is set to true. */ - public @Nullable Boolean checked; + public Boolean checked; /** * Asserts that the element is in the indeterminate (mixed) state. Only supported for checkboxes and radio buttons. This * option can't be true when {@code checked} is provided. */ - public @Nullable Boolean indeterminate; + public Boolean indeterminate; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Provides state to assert for. Asserts for input to be checked by default. This option can't be used when {@code @@ -104,7 +269,7 @@ class IsDisabledOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -115,11 +280,11 @@ public IsDisabledOptions setTimeout(double timeout) { } } class IsEditableOptions { - public @Nullable Boolean editable; + public Boolean editable; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; public IsEditableOptions setEditable(boolean editable) { this.editable = editable; @@ -137,7 +302,7 @@ class IsEmptyOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -148,11 +313,11 @@ public IsEmptyOptions setTimeout(double timeout) { } } class IsEnabledOptions { - public @Nullable Boolean enabled; + public Boolean enabled; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; public IsEnabledOptions setEnabled(boolean enabled) { this.enabled = enabled; @@ -170,7 +335,7 @@ class IsFocusedOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -184,7 +349,7 @@ class IsHiddenOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -199,11 +364,11 @@ class IsInViewportOptions { * The minimal ratio of the element to intersect viewport. If equals to {@code 0}, then element should intersect viewport * at any positive ratio. Defaults to {@code 0}. */ - public @Nullable Double ratio; + public Double ratio; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * The minimal ratio of the element to intersect viewport. If equals to {@code 0}, then element should intersect viewport @@ -225,8 +390,8 @@ class IsVisibleOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; - public @Nullable Boolean visible; + public Double timeout; + public Boolean visible; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -244,7 +409,7 @@ class ContainsClassOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -259,15 +424,15 @@ class ContainsTextOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression flag if specified. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to use {@code element.innerText} instead of {@code element.textContent} when retrieving DOM node text. */ - public @Nullable Boolean useInnerText; + public Boolean useInnerText; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -297,11 +462,11 @@ class HasAccessibleDescriptionOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression flag if specified. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -324,11 +489,11 @@ class HasAccessibleErrorMessageOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression flag if specified. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -351,11 +516,11 @@ class HasAccessibleNameOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression flag if specified. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -378,11 +543,11 @@ class HasAttributeOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression flag if specified. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -404,7 +569,7 @@ class HasClassOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -418,7 +583,7 @@ class HasCountOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -432,11 +597,11 @@ class HasCSSOptions { /** * Pseudo-element to read computed styles from. */ - public @Nullable PseudoElement pseudo; + public PseudoElement pseudo; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Pseudo-element to read computed styles from. @@ -457,7 +622,7 @@ class HasIdOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -471,7 +636,7 @@ class HasJSPropertyOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -485,7 +650,7 @@ class HasRoleOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -500,15 +665,15 @@ class HasTextOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression flag if specified. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to use {@code element.innerText} instead of {@code element.textContent} when retrieving DOM node text. */ - public @Nullable Boolean useInnerText; + public Boolean useInnerText; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -537,7 +702,7 @@ class HasValueOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -551,7 +716,7 @@ class HasValuesOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -565,7 +730,7 @@ class MatchesAriaSnapshotOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -613,7 +778,7 @@ default void isAttached() { * * @since v1.33 */ - void isAttached(@Nullable IsAttachedOptions options); + void isAttached(IsAttachedOptions options); /** * Ensures the {@code Locator} points to a checked input. * @@ -637,7 +802,7 @@ default void isChecked() { * * @since v1.20 */ - void isChecked(@Nullable IsCheckedOptions options); + void isChecked(IsCheckedOptions options); /** * Ensures the {@code Locator} points to a disabled element. Element is disabled if it has "disabled" attribute or is * disabled via non-visible one. @@ -793,7 +958,7 @@ default void isHidden() { * * @since v1.20 */ - void isHidden(@Nullable IsHiddenOptions options); + void isHidden(IsHiddenOptions options); /** * Ensures the {@code Locator} points to an element that intersects viewport, according to the intersection observer API. @@ -831,7 +996,7 @@ default void isInViewport() { * * @since v1.31 */ - void isInViewport(@Nullable IsInViewportOptions options); + void isInViewport(IsInViewportOptions options); /** * Ensures that {@code Locator} points to an attached and visible DOM node. @@ -885,7 +1050,7 @@ default void isVisible() { * * @since v1.20 */ - void isVisible(@Nullable IsVisibleOptions options); + void isVisible(IsVisibleOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. All classes from the asserted value, separated * by spaces, must be present in the expected) { * @param expected A string containing expected class names, separated by spaces, or a list of such strings to assert multiple elements. * @since v1.52 */ - void containsClass(List expected, @Nullable ContainsClassOptions options); + void containsClass(List expected, ContainsClassOptions options); /** * Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered * when computing the text content of the element. You can use regular expressions for the value as well. @@ -1065,7 +1230,7 @@ default void containsText(String expected) { * @param expected Expected substring or RegExp or a list of those. * @since v1.20 */ - void containsText(String expected, @Nullable ContainsTextOptions options); + void containsText(String expected, ContainsTextOptions options); /** * Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered * when computing the text content of the element. You can use regular expressions for the value as well. @@ -1153,7 +1318,7 @@ default void containsText(Pattern expected) { * @param expected Expected substring or RegExp or a list of those. * @since v1.20 */ - void containsText(Pattern expected, @Nullable ContainsTextOptions options); + void containsText(Pattern expected, ContainsTextOptions options); /** * Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered * when computing the text content of the element. You can use regular expressions for the value as well. @@ -1241,7 +1406,7 @@ default void containsText(String[] expected) { * @param expected Expected substring or RegExp or a list of those. * @since v1.20 */ - void containsText(String[] expected, @Nullable ContainsTextOptions options); + void containsText(String[] expected, ContainsTextOptions options); /** * Ensures the {@code Locator} points to an element that contains the given text. All nested elements will be considered * when computing the text content of the element. You can use regular expressions for the value as well. @@ -1329,7 +1494,7 @@ default void containsText(Pattern[] expected) { * @param expected Expected substring or RegExp or a list of those. * @since v1.20 */ - void containsText(Pattern[] expected, @Nullable ContainsTextOptions options); + void containsText(Pattern[] expected, ContainsTextOptions options); /** * Ensures the {@code Locator} points to an element with a given accessible description. @@ -1359,7 +1524,7 @@ default void hasAccessibleDescription(String description) { * @param description Expected accessible description. * @since v1.44 */ - void hasAccessibleDescription(String description, @Nullable HasAccessibleDescriptionOptions options); + void hasAccessibleDescription(String description, HasAccessibleDescriptionOptions options); /** * Ensures the {@code Locator} points to an element with a given accessible description. @@ -1389,7 +1554,7 @@ default void hasAccessibleDescription(Pattern description) { * @param description Expected accessible description. * @since v1.44 */ - void hasAccessibleDescription(Pattern description, @Nullable HasAccessibleDescriptionOptions options); + void hasAccessibleDescription(Pattern description, HasAccessibleDescriptionOptions options); /** * Ensures the {@code Locator} points to an element with a given aria errormessage. @@ -1419,7 +1584,7 @@ default void hasAccessibleErrorMessage(String errorMessage) { * @param errorMessage Expected accessible error message. * @since v1.50 */ - void hasAccessibleErrorMessage(String errorMessage, @Nullable HasAccessibleErrorMessageOptions options); + void hasAccessibleErrorMessage(String errorMessage, HasAccessibleErrorMessageOptions options); /** * Ensures the {@code Locator} points to an element with a given aria errormessage. @@ -1449,7 +1614,7 @@ default void hasAccessibleErrorMessage(Pattern errorMessage) { * @param errorMessage Expected accessible error message. * @since v1.50 */ - void hasAccessibleErrorMessage(Pattern errorMessage, @Nullable HasAccessibleErrorMessageOptions options); + void hasAccessibleErrorMessage(Pattern errorMessage, HasAccessibleErrorMessageOptions options); /** * Ensures the {@code Locator} points to an element with a given accessible name. @@ -1479,7 +1644,7 @@ default void hasAccessibleName(String name) { * @param name Expected accessible name. * @since v1.44 */ - void hasAccessibleName(String name, @Nullable HasAccessibleNameOptions options); + void hasAccessibleName(String name, HasAccessibleNameOptions options); /** * Ensures the {@code Locator} points to an element with a given accessible name. @@ -1509,7 +1674,7 @@ default void hasAccessibleName(Pattern name) { * @param name Expected accessible name. * @since v1.44 */ - void hasAccessibleName(Pattern name, @Nullable HasAccessibleNameOptions options); + void hasAccessibleName(Pattern name, HasAccessibleNameOptions options); /** * Ensures the {@code Locator} points to an element with given attribute. * @@ -1537,7 +1702,7 @@ default void hasAttribute(String name, String value) { * @param value Expected attribute value. * @since v1.20 */ - void hasAttribute(String name, String value, @Nullable HasAttributeOptions options); + void hasAttribute(String name, String value, HasAttributeOptions options); /** * Ensures the {@code Locator} points to an element with given attribute. * @@ -1565,7 +1730,7 @@ default void hasAttribute(String name, Pattern value) { * @param value Expected attribute value. * @since v1.20 */ - void hasAttribute(String name, Pattern value, @Nullable HasAttributeOptions options); + void hasAttribute(String name, Pattern value, HasAttributeOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match * the element's {@code class} attribute. To match individual classes use {@link @@ -1611,7 +1776,7 @@ default void hasClass(String expected) { * @param expected Expected class or RegExp or a list of those. * @since v1.20 */ - void hasClass(String expected, @Nullable HasClassOptions options); + void hasClass(String expected, HasClassOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match * the element's {@code class} attribute. To match individual classes use {@link @@ -1657,7 +1822,7 @@ default void hasClass(Pattern expected) { * @param expected Expected class or RegExp or a list of those. * @since v1.20 */ - void hasClass(Pattern expected, @Nullable HasClassOptions options); + void hasClass(Pattern expected, HasClassOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match * the element's {@code class} attribute. To match individual classes use {@link @@ -1703,7 +1868,7 @@ default void hasClass(String[] expected) { * @param expected Expected class or RegExp or a list of those. * @since v1.20 */ - void hasClass(String[] expected, @Nullable HasClassOptions options); + void hasClass(String[] expected, HasClassOptions options); /** * Ensures the {@code Locator} points to an element with given CSS classes. When a string is provided, it must fully match * the element's {@code class} attribute. To match individual classes use {@link @@ -1749,7 +1914,7 @@ default void hasClass(Pattern[] expected) { * @param expected Expected class or RegExp or a list of those. * @since v1.20 */ - void hasClass(Pattern[] expected, @Nullable HasClassOptions options); + void hasClass(Pattern[] expected, HasClassOptions options); /** * Ensures the {@code Locator} resolves to an exact number of DOM nodes. * @@ -1775,7 +1940,7 @@ default void hasCount(int count) { * @param count Expected count. * @since v1.20 */ - void hasCount(int count, @Nullable HasCountOptions options); + void hasCount(int count, HasCountOptions options); /** * Ensures the {@code Locator} resolves to an element with the given computed CSS style. * @@ -1803,7 +1968,7 @@ default void hasCSS(String name, String value) { * @param value CSS property value. * @since v1.20 */ - void hasCSS(String name, String value, @Nullable HasCSSOptions options); + void hasCSS(String name, String value, HasCSSOptions options); /** * Ensures the {@code Locator} resolves to an element with the given computed CSS style. * @@ -1831,7 +1996,7 @@ default void hasCSS(String name, Pattern value) { * @param value CSS property value. * @since v1.20 */ - void hasCSS(String name, Pattern value, @Nullable HasCSSOptions options); + void hasCSS(String name, Pattern value, HasCSSOptions options); /** * Ensures the {@code Locator} points to an element with the given DOM Node ID. * @@ -1857,7 +2022,7 @@ default void hasId(String id) { * @param id Element id. * @since v1.20 */ - void hasId(String id, @Nullable HasIdOptions options); + void hasId(String id, HasIdOptions options); /** * Ensures the {@code Locator} points to an element with the given DOM Node ID. * @@ -1883,7 +2048,7 @@ default void hasId(Pattern id) { * @param id Element id. * @since v1.20 */ - void hasId(Pattern id, @Nullable HasIdOptions options); + void hasId(Pattern id, HasIdOptions options); /** * Ensures the {@code Locator} points to an element with given JavaScript property. Note that this property can be of a * primitive type as well as a plain serializable JavaScript object. @@ -1913,7 +2078,7 @@ default void hasJSProperty(String name, Object value) { * @param value Property value. * @since v1.20 */ - void hasJSProperty(String name, Object value, @Nullable HasJSPropertyOptions options); + void hasJSProperty(String name, Object value, HasJSPropertyOptions options); /** * Ensures the {@code Locator} points to an element with a given ARIA * role. @@ -1949,7 +2114,7 @@ default void hasRole(AriaRole role) { * @param role Required aria role. * @since v1.44 */ - void hasRole(AriaRole role, @Nullable HasRoleOptions options); + void hasRole(AriaRole role, HasRoleOptions options); /** * Ensures the {@code Locator} points to an element with the given text. All nested elements will be considered when * computing the text content of the element. You can use regular expressions for the value as well. @@ -2037,7 +2202,7 @@ default void hasText(String expected) { * @param expected Expected string or RegExp or a list of those. * @since v1.20 */ - void hasText(String expected, @Nullable HasTextOptions options); + void hasText(String expected, HasTextOptions options); /** * Ensures the {@code Locator} points to an element with the given text. All nested elements will be considered when * computing the text content of the element. You can use regular expressions for the value as well. @@ -2125,7 +2290,7 @@ default void hasText(Pattern expected) { * @param expected Expected string or RegExp or a list of those. * @since v1.20 */ - void hasText(Pattern expected, @Nullable HasTextOptions options); + void hasText(Pattern expected, HasTextOptions options); /** * Ensures the {@code Locator} points to an element with the given text. All nested elements will be considered when * computing the text content of the element. You can use regular expressions for the value as well. @@ -2213,7 +2378,7 @@ default void hasText(String[] expected) { * @param expected Expected string or RegExp or a list of those. * @since v1.20 */ - void hasText(String[] expected, @Nullable HasTextOptions options); + void hasText(String[] expected, HasTextOptions options); /** * Ensures the {@code Locator} points to an element with the given text. All nested elements will be considered when * computing the text content of the element. You can use regular expressions for the value as well. @@ -2301,7 +2466,7 @@ default void hasText(Pattern[] expected) { * @param expected Expected string or RegExp or a list of those. * @since v1.20 */ - void hasText(Pattern[] expected, @Nullable HasTextOptions options); + void hasText(Pattern[] expected, HasTextOptions options); /** * Ensures the {@code Locator} points to an element with the given input value. You can use regular expressions for the * value as well. @@ -2329,7 +2494,7 @@ default void hasValue(String value) { * @param value Expected value. * @since v1.20 */ - void hasValue(String value, @Nullable HasValueOptions options); + void hasValue(String value, HasValueOptions options); /** * Ensures the {@code Locator} points to an element with the given input value. You can use regular expressions for the * value as well. @@ -2357,7 +2522,7 @@ default void hasValue(Pattern value) { * @param value Expected value. * @since v1.20 */ - void hasValue(Pattern value, @Nullable HasValueOptions options); + void hasValue(Pattern value, HasValueOptions options); /** * Ensures the {@code Locator} points to multi-select/combobox (i.e. a {@code select} with the {@code multiple} attribute) * and the specified values are selected. @@ -2391,7 +2556,7 @@ default void hasValues(String[] values) { * @param values Expected options currently selected. * @since v1.23 */ - void hasValues(String[] values, @Nullable HasValuesOptions options); + void hasValues(String[] values, HasValuesOptions options); /** * Ensures the {@code Locator} points to multi-select/combobox (i.e. a {@code select} with the {@code multiple} attribute) * and the specified values are selected. @@ -2425,7 +2590,7 @@ default void hasValues(Pattern[] values) { * @param values Expected options currently selected. * @since v1.23 */ - void hasValues(Pattern[] values, @Nullable HasValuesOptions options); + void hasValues(Pattern[] values, HasValuesOptions options); /** * Asserts that the target element matches the given accessibility snapshot. @@ -2459,6 +2624,53 @@ default void matchesAriaSnapshot(String expected) { * * @since v1.49 */ - void matchesAriaSnapshot(String expected, @Nullable MatchesAriaSnapshotOptions options); + 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 + * test-runner-managed snapshot directory or configuration 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} extension. + * @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} extension. + * @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 path. The last segment must have a {@code .png} extension. + * @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 path. The last segment must have a {@code .png} extension. + * @since v1.23 + */ + void hasScreenshot(String[] nameSegments, HasScreenshotOptions options); } From e8a10420a3041fa6b178b9906ffe3e9983875aac Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:16 +0530 Subject: [PATCH 2/9] feat: add visual comparison assertions --- .../playwright/assertions/PageAssertions.java | 267 +++++++++++++++++- 1 file changed, 257 insertions(+), 10 deletions(-) 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 b34467b7c..be4e417e3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java +++ b/playwright/src/main/java/com/microsoft/playwright/assertions/PageAssertions.java @@ -16,8 +16,13 @@ package com.microsoft.playwright.assertions; -import org.jspecify.annotations.Nullable; +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} @@ -42,7 +47,7 @@ class MatchesAriaSnapshotOptions { /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -52,11 +57,197 @@ 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: + *

+ * + *

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: + *

+ * + *

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}. */ - public @Nullable Double timeout; + public Double timeout; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. @@ -71,11 +262,11 @@ class HasURLOptions { * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular * expression parameter if specified. A provided predicate ignores this flag. */ - public @Nullable Boolean ignoreCase; + public Boolean ignoreCase; /** * Time to retry the assertion for in milliseconds. Defaults to {@code 5000}. */ - public @Nullable Double timeout; + public Double timeout; /** * Whether to perform case-insensitive match. {@code ignoreCase} option takes precedence over the corresponding regular @@ -139,7 +330,63 @@ default void matchesAriaSnapshot(String expected) { * * @since v1.60 */ - void matchesAriaSnapshot(String expected, @Nullable MatchesAriaSnapshotOptions options); + 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 + * test-runner-managed snapshot directory or configuration 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} extension. + * @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} extension. + * @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 path. The last segment must have a {@code .png} extension. + * @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 path. The last segment must have a {@code .png} extension. + * @since v1.23 + */ + void hasScreenshot(String[] nameSegments, HasScreenshotOptions options); /** * Ensures the page has the given title. * @@ -165,7 +412,7 @@ default void hasTitle(String titleOrRegExp) { * @param titleOrRegExp Expected title or RegExp. * @since v1.20 */ - void hasTitle(String titleOrRegExp, @Nullable HasTitleOptions options); + void hasTitle(String titleOrRegExp, HasTitleOptions options); /** * Ensures the page has the given title. * @@ -191,7 +438,7 @@ default void hasTitle(Pattern titleOrRegExp) { * @param titleOrRegExp Expected title or RegExp. * @since v1.20 */ - void hasTitle(Pattern titleOrRegExp, @Nullable HasTitleOptions options); + void hasTitle(Pattern titleOrRegExp, HasTitleOptions options); /** * Ensures the page is navigated to the given URL. * @@ -217,7 +464,7 @@ default void hasURL(String urlOrRegExp) { * @param urlOrRegExp Expected URL string or RegExp. * @since v1.20 */ - void hasURL(String urlOrRegExp, @Nullable HasURLOptions options); + void hasURL(String urlOrRegExp, HasURLOptions options); /** * Ensures the page is navigated to the given URL. * @@ -243,6 +490,6 @@ default void hasURL(Pattern urlOrRegExp) { * @param urlOrRegExp Expected URL string or RegExp. * @since v1.20 */ - void hasURL(Pattern urlOrRegExp, @Nullable HasURLOptions options); + void hasURL(Pattern urlOrRegExp, HasURLOptions options); } From a149fedb151c79369aa0ca57b5c4634c42412018 Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:19 +0530 Subject: [PATCH 3/9] feat: add visual comparison assertions --- .../playwright/impl/LocatorAssertionsImpl.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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..9d8a4761d 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,21 @@ 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); + } + + 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) { From eac225ed4236c97b70c7d9678026e92dadb82f04 Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:22 +0530 Subject: [PATCH 4/9] feat: add visual comparison assertions --- .../playwright/impl/PageAssertionsImpl.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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..5f3f51cd0 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,21 @@ 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); + } + + 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); From 94cbd80ba2ab122c68d6f706c5d099284fff1577 Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:26 +0530 Subject: [PATCH 5/9] feat: add visual comparison assertions --- .../microsoft/playwright/impl/PageImpl.java | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) 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 88aaf4c32..bbb71b6c3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -88,7 +88,6 @@ private static final Map eventSubscriptions() { Map result = new HashMap<>(); result.put(EventType.CONSOLE, "console"); result.put(EventType.DIALOG, "dialog"); - result.put(EventType.DIALOGCLOSED, "dialogClosed"); result.put(EventType.REQUEST, "request"); result.put(EventType.RESPONSE, "response"); result.put(EventType.REQUESTFINISHED, "requestFinished"); @@ -111,7 +110,6 @@ enum EventType { CONSOLE, CRASH, DIALOG, - DIALOGCLOSED, DOMCONTENTLOADED, DOWNLOAD, FILECHOOSER, @@ -305,16 +303,6 @@ public void offDialog(Consumer handler) { listeners.remove(EventType.DIALOG, handler); } - @Override - public void onDialogClosed(Consumer handler) { - listeners.add(EventType.DIALOGCLOSED, handler); - } - - @Override - public void offDialogClosed(Consumer handler) { - listeners.remove(EventType.DIALOGCLOSED, handler); - } - @Override public void onDOMContentLoaded(Consumer handler) { listeners.add(EventType.DOMCONTENTLOADED, handler); @@ -1259,6 +1247,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(); From a0bb45b4fcbb561b35b013a97948c3acc6f34932 Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:30 +0530 Subject: [PATCH 6/9] feat: add visual comparison assertions --- .../microsoft/playwright/impl/Protocol.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) 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..31cfd4171 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Protocol.java @@ -17,6 +17,10 @@ package com.microsoft.playwright.impl; import java.util.List; +import com.microsoft.playwright.options.Clip; +import com.microsoft.playwright.options.ScreenshotCaret; +import com.microsoft.playwright.options.ScreenshotAnimations; +import com.microsoft.playwright.options.ScreenshotScale; class Channel { String guid; @@ -128,4 +132,37 @@ class FrameExpectErrorDetails { String customErrorMessage; } +class PageExpectScreenshotOptions { + String expected; + boolean isNot; + LocatorImpl locator; + String comparator; + Integer maxDiffPixels; + Double maxDiffPixelRatio; + Double threshold; + Boolean fullPage; + Clip clip; + Boolean omitBackground; + ScreenshotCaret caret; + ScreenshotAnimations animations; + 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; +} + From dcdb7a3a7a0411a086a9dc410cd0cdddec990112 Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:36 +0530 Subject: [PATCH 7/9] feat: add visual comparison assertions --- .../impl/ScreenshotAssertionsHelper.java | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsHelper.java 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..99f7e3401 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsHelper.java @@ -0,0 +1,264 @@ +/* + * 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.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Locale; + +// Java-side implementation of `hasScreenshot()`. The pixel-level comparison +// itself is done by the driver via "Page.expectScreenshot"; this class only +// resolves the baseline file and decides whether to create/update/compare it. +// +// There is no test runner here to derive a snapshot name from, so a name is +// always required explicitly. Baselines are stored under +// "src/test/resources/__screenshots__/", overridable via the +// "playwright.snapshotDir" system property; "-Dplaywright.updateSnapshots=true" +// (re-)generates baselines. +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 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); + + PageExpectScreenshotOptions protocolOptions = toProtocolOptions(options); + 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)); + } + writeFile(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) { + 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; + result.maxDiffPixelRatio = options.maxDiffPixelRatio; + result.threshold = options.threshold; + result.maskColor = options.maskColor; + result.style = options.style; + 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[] segments = toSegments(nameOrNames); + String lastSegment = segments[segments.length - 1]; + // The driver's comparator only supports PNG ("Only PNG screenshots are supported"). + if (!lastSegment.toLowerCase(Locale.ROOT).endsWith(".png")) { + throw new PlaywrightException("Screenshot name \"" + lastSegment + "\" must have a '.png' extension"); + } + String baseDir = System.getProperty(SNAPSHOT_DIR_PROPERTY, DEFAULT_SNAPSHOT_DIR); + Path snapshotDir = Paths.get(baseDir).toAbsolutePath().normalize(); + Path path = snapshotDir; + for (String segment : segments) { + Path segmentPath = Paths.get(segment); + if (segmentPath.isAbsolute()) { + throw new PlaywrightException("Screenshot name must be relative: " + segment); + } + path = path.resolve(segmentPath).normalize(); + } + + if (!path.startsWith(snapshotDir)) { + throw new PlaywrightException("Screenshot name resolves outside the snapshot directory: " + path); + } + return path; + } + + private static String[] toSegments(Object nameOrNames) { + if (nameOrNames instanceof String[]) { + String[] segments = (String[]) nameOrNames; + if (segments.length == 0) { + throw new PlaywrightException("Screenshot name segments must not be empty"); + } + for (String segment : segments) { + if (segment == null || segment.isEmpty()) { + throw new PlaywrightException("Screenshot name segments must not be null or empty"); + } + } + return segments; + } + if (nameOrNames instanceof String && !((String) nameOrNames).isEmpty()) { + return new String[] { (String) nameOrNames }; + } + throw new PlaywrightException( + "A screenshot name is required, for example: assertThat(page).hasScreenshot(\"example.png\")"); + } + + 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) { + writeFile(result.actual, actualDebugPath(expectedPath)); + } + if (result.diff != null) { + writeFile(result.diff, diffDebugPath(expectedPath)); + } + } + + private static void writeFile(byte[] bytes, Path path) { + Path temp = null; + try { + temp = Files.createTempFile(path.getParent(), path.getFileName().toString(), ".tmp"); + Utils.writeToFile(bytes, temp); + try { + Files.move(temp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temp, path, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + try { + if (temp != null) { + Files.deleteIfExists(temp); + } + } catch (IOException ignored) { + // Preserve the original write failure. + } + throw new PlaywrightException("Failed to write screenshot file: " + path, e); + } + } + + private static String callLog(List log) { + if (log == null || log.isEmpty()) { + return ""; + } + return "\nCall log:\n" + String.join("\n", log); + } +} From a6480305805837922d83d479f840c5e9666f9ade Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:38 +0530 Subject: [PATCH 8/9] feat: add visual comparison assertions --- .../impl/ScreenshotAssertionsOptions.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/ScreenshotAssertionsOptions.java 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; +} From 03e756299b23efe03f99c062d014a73dba3eae2d Mon Sep 17 00:00:00 2001 From: Anil Kumar Reddy Gaddam Date: Thu, 24 Sep 2026 15:59:43 +0530 Subject: [PATCH 9/9] feat: add visual comparison assertions --- .../playwright/TestScreenshotAssertions.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestScreenshotAssertions.java 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..0de6c7a3d --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestScreenshotAssertions.java @@ -0,0 +1,127 @@ +/* + * 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__"); + + @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"))); + } + + @Test + void shouldSupportNameSegments() { + page.setContent("
"); + assertThat(page).hasScreenshot(new String[] {"nested", "page-nested.png"}); + assertTrue(Files.exists(SNAPSHOT_ROOT.resolve("nested").resolve("page-nested.png"))); + } + + @Test + void shouldRejectNonPngExtension() { + page.setContent("
Hello
"); + PlaywrightException e = assertThrows(PlaywrightException.class, () -> + assertThat(page).hasScreenshot("image.webp")); + assertTrue(e.getMessage().contains(".png"), e.getMessage()); + } + + @Test + void shouldRejectSnapshotPathTraversal() { + page.setContent("
Hello
"); + + PlaywrightException e = assertThrows(PlaywrightException.class, () -> + assertThat(page).hasScreenshot("../outside.png")); + assertTrue(e.getMessage().contains("outside the snapshot directory"), e.getMessage()); + + PlaywrightException segmentException = assertThrows(PlaywrightException.class, () -> + assertThat(page).hasScreenshot(new String[] {"..", "outside.png"})); + assertTrue(segmentException.getMessage().contains("outside the snapshot directory"), segmentException.getMessage()); + } + + @Test + void shouldRequireAnExplicitName() { + page.setContent("
Hello
"); + assertThrows(PlaywrightException.class, () -> assertThat(page).hasScreenshot((String) null)); + } +}