From 072931d944b6e23781fad0d6f47f3e48113cbb62 Mon Sep 17 00:00:00 2001 From: Neha Sanserwal Date: Wed, 22 Apr 2026 19:55:05 +0530 Subject: [PATCH 1/5] Add Cucumber step definitions for Percy visual testing Add PercySteps class providing Gherkin step definitions for: - Percy Snapshot (DOM): widths, min height, percy CSS, scope, layout mode, JavaScript, labels, test case, responsive capture - Create Percy Region: ignore/consider/intelliignore by CSS, XPath, bounding box, with diff sensitivity and padding - Data table support for arbitrary options Cucumber dependency is provided scope - users bring their own version. Includes unit tests for all step definitions. Co-Authored-By: Claude Opus 4.6 (1M context) --- pom.xml | 13 + .../percy/playwright/cucumber/PercySteps.java | 314 ++++++++++++++++++ .../playwright/cucumber/PercyStepsTest.java | 124 +++++++ 3 files changed, 451 insertions(+) create mode 100644 src/main/java/io/percy/playwright/cucumber/PercySteps.java create mode 100644 src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java diff --git a/pom.xml b/pom.xml index 666daed..63339c8 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,19 @@ ${junit.jupiter.version} test + + + io.cucumber + cucumber-java + 7.15.0 + provided + + + io.cucumber + cucumber-junit-platform-engine + 7.15.0 + test + diff --git a/src/main/java/io/percy/playwright/cucumber/PercySteps.java b/src/main/java/io/percy/playwright/cucumber/PercySteps.java new file mode 100644 index 0000000..99b5c6f --- /dev/null +++ b/src/main/java/io/percy/playwright/cucumber/PercySteps.java @@ -0,0 +1,314 @@ +package io.percy.playwright.cucumber; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.When; +import io.cucumber.java.en.Then; +import io.percy.playwright.Percy; + +import com.microsoft.playwright.Page; + +import java.util.*; + +/** + * Cucumber step definitions for Percy visual testing with Playwright. + * + *

Provides Gherkin steps to capture Percy snapshots and define + * ignore/consider regions from Cucumber feature files.

+ * + *

Usage in feature files:

+ *
+ * Feature: Visual Testing
+ *   Scenario: Homepage visual test
+ *     Given I have a Percy instance
+ *     When I take a Percy snapshot named "Homepage"
+ *
+ *   Scenario: Snapshot with options
+ *     Given I have a Percy instance
+ *     When I take a Percy snapshot named "Responsive" with widths "375,768,1280"
+ *
+ *   Scenario: Ignore region
+ *     Given I have a Percy instance
+ *     And I create a Percy ignore region with CSS selector ".ad-banner"
+ *     When I take a Percy snapshot named "No Ads" with regions
+ * 
+ * + *

Setup in step definition glue:

+ *
+ * public class Hooks {
+ *     private static Playwright playwright;
+ *     private static Browser browser;
+ *     private static Page page;
+ *
+ *     {@literal @}Before
+ *     public void setUp() {
+ *         playwright = Playwright.create();
+ *         browser = playwright.chromium().launch();
+ *         page = browser.newPage();
+ *         PercySteps.setPage(page);
+ *     }
+ *
+ *     {@literal @}After
+ *     public void tearDown() {
+ *         if (browser != null) browser.close();
+ *         if (playwright != null) playwright.close();
+ *         PercySteps.reset();
+ *     }
+ * }
+ * 
+ */ +public class PercySteps { + + private static Page page; + private static Percy percy; + private static List> regions = new ArrayList<>(); + + /** + * Set the Playwright Page instance for Percy to use. + * Call this from your Cucumber hooks before using any Percy steps. + * + * @param playwrightPage the Playwright Page instance + */ + public static void setPage(Page playwrightPage) { + page = playwrightPage; + percy = new Percy(page); + } + + /** + * Get the current Percy instance. + * + * @return the Percy instance, or null if not initialized + */ + public static Percy getPercy() { + return percy; + } + + /** + * Reset the Percy instance and clear stored regions. + * Call this from your Cucumber hooks in teardown. + */ + public static void reset() { + percy = null; + page = null; + regions.clear(); + } + + // ------------------------------------------------------------------ + // Given steps + // ------------------------------------------------------------------ + + @Given("I have a Percy instance") + public void iHaveAPercyInstance() { + if (page == null) { + throw new IllegalStateException( + "Playwright Page not set. Call PercySteps.setPage(page) in your @Before hook."); + } + if (percy == null) { + percy = new Percy(page); + } + } + + @Given("I create a Percy ignore region with CSS selector {string}") + public void iCreateIgnoreRegionCSS(String cssSelector) { + Map params = new HashMap<>(); + params.put("algorithm", "ignore"); + params.put("elementCSS", cssSelector); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy ignore region with XPath {string}") + public void iCreateIgnoreRegionXPath(String xpath) { + Map params = new HashMap<>(); + params.put("algorithm", "ignore"); + params.put("elementXpath", xpath); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy ignore region with bounding box {int}, {int}, {int}, {int}") + public void iCreateIgnoreRegionBoundingBox(int x, int y, int width, int height) { + Map boundingBox = new HashMap<>(); + boundingBox.put("x", x); + boundingBox.put("y", y); + boundingBox.put("width", width); + boundingBox.put("height", height); + + Map params = new HashMap<>(); + params.put("algorithm", "ignore"); + params.put("boundingBox", boundingBox); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy consider region with CSS selector {string}") + public void iCreateConsiderRegionCSS(String cssSelector) { + Map params = new HashMap<>(); + params.put("algorithm", "standard"); + params.put("elementCSS", cssSelector); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy consider region with CSS selector {string} and diff sensitivity {int}") + public void iCreateConsiderRegionCSSWithSensitivity(String cssSelector, int sensitivity) { + Map params = new HashMap<>(); + params.put("algorithm", "standard"); + params.put("elementCSS", cssSelector); + params.put("diffSensitivity", sensitivity); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy intelliignore region with CSS selector {string}") + public void iCreateIntelliIgnoreRegionCSS(String cssSelector) { + Map params = new HashMap<>(); + params.put("algorithm", "intelliignore"); + params.put("elementCSS", cssSelector); + regions.add(percy.createRegion(params)); + } + + @Given("I clear Percy regions") + public void iClearPercyRegions() { + regions.clear(); + } + + // ------------------------------------------------------------------ + // When steps - Snapshot (DOM) + // ------------------------------------------------------------------ + + @When("I take a Percy snapshot named {string}") + public void iTakeSnapshot(String name) { + percy.snapshot(name); + } + + @When("I take a Percy snapshot named {string} with widths {string}") + public void iTakeSnapshotWithWidths(String name, String widths) { + Map options = new HashMap<>(); + options.put("widths", parseWidths(widths)); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with min height {int}") + public void iTakeSnapshotWithMinHeight(String name, int minHeight) { + Map options = new HashMap<>(); + options.put("minHeight", minHeight); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with Percy CSS {string}") + public void iTakeSnapshotWithCSS(String name, String percyCSS) { + Map options = new HashMap<>(); + options.put("percyCSS", percyCSS); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with scope {string}") + public void iTakeSnapshotWithScope(String name, String scope) { + Map options = new HashMap<>(); + options.put("scope", scope); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with layout mode") + public void iTakeSnapshotWithLayout(String name) { + Map options = new HashMap<>(); + options.put("enableLayout", true); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with JavaScript enabled") + public void iTakeSnapshotWithJS(String name) { + Map options = new HashMap<>(); + options.put("enableJavaScript", true); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with labels {string}") + public void iTakeSnapshotWithLabels(String name, String labels) { + Map options = new HashMap<>(); + options.put("labels", labels); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with test case {string}") + public void iTakeSnapshotWithTestCase(String name, String testCase) { + Map options = new HashMap<>(); + options.put("testCase", testCase); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with regions") + public void iTakeSnapshotWithRegions(String name) { + Map options = new HashMap<>(); + if (!regions.isEmpty()) { + options.put("regions", new ArrayList<>(regions)); + regions.clear(); + } + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with widths {string} and regions") + public void iTakeSnapshotWithWidthsAndRegions(String name, String widths) { + Map options = new HashMap<>(); + options.put("widths", parseWidths(widths)); + if (!regions.isEmpty()) { + options.put("regions", new ArrayList<>(regions)); + regions.clear(); + } + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with options:") + public void iTakeSnapshotWithOptions(String name, Map optionsTable) { + Map options = buildOptions(optionsTable); + if (!regions.isEmpty()) { + options.put("regions", new ArrayList<>(regions)); + regions.clear(); + } + percy.snapshot(name, options); + } + + // ------------------------------------------------------------------ + // Then steps + // ------------------------------------------------------------------ + + @Then("Percy should be enabled") + public void percyShouldBeEnabled() { + if (percy == null) { + throw new IllegalStateException("Percy instance not initialized."); + } + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private static List parseWidths(String widths) { + List result = new ArrayList<>(); + for (String w : widths.split(",")) { + result.add(Integer.parseInt(w.trim())); + } + return result; + } + + private static Map buildOptions(Map table) { + Map options = new HashMap<>(); + for (Map.Entry entry : table.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + switch (key) { + case "widths": + options.put("widths", parseWidths(value)); + break; + case "minHeight": + options.put("minHeight", Integer.parseInt(value)); + break; + case "enableJavaScript": + case "enableLayout": + case "disableShadowDom": + case "responsiveSnapshotCapture": + options.put(key, Boolean.parseBoolean(value)); + break; + default: + options.put(key, value); + break; + } + } + return options; + } +} diff --git a/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java b/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java new file mode 100644 index 0000000..89225a3 --- /dev/null +++ b/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java @@ -0,0 +1,124 @@ +package io.percy.playwright.cucumber; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.percy.playwright.Percy; +import com.microsoft.playwright.Page; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PercyStepsTest { + + private Page mockPage; + + @BeforeEach + void setUp() { + mockPage = mock(Page.class); + } + + @AfterEach + void tearDown() { + PercySteps.reset(); + } + + @Test + void testSetPageAndGetPercy() { + PercySteps.setPage(mockPage); + assertNotNull(PercySteps.getPercy()); + } + + @Test + void testResetClearsState() { + PercySteps.setPage(mockPage); + assertNotNull(PercySteps.getPercy()); + + PercySteps.reset(); + assertNull(PercySteps.getPercy()); + } + + @Test + void testIHaveAPercyInstanceThrowsWithoutPage() { + PercySteps steps = new PercySteps(); + assertThrows(IllegalStateException.class, steps::iHaveAPercyInstance); + } + + @Test + void testIHaveAPercyInstanceSucceedsWithPage() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + assertDoesNotThrow(steps::iHaveAPercyInstance); + } + + @Test + void testCreateIgnoreRegionCSS() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + assertDoesNotThrow(() -> steps.iCreateIgnoreRegionCSS(".ad-banner")); + } + + @Test + void testCreateIgnoreRegionXPath() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + assertDoesNotThrow(() -> steps.iCreateIgnoreRegionXPath("//div[@id='header']")); + } + + @Test + void testCreateIgnoreRegionBoundingBox() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + assertDoesNotThrow(() -> steps.iCreateIgnoreRegionBoundingBox(0, 0, 200, 100)); + } + + @Test + void testCreateConsiderRegionCSS() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + assertDoesNotThrow(() -> steps.iCreateConsiderRegionCSS(".content")); + } + + @Test + void testCreateConsiderRegionWithSensitivity() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + assertDoesNotThrow( + () -> steps.iCreateConsiderRegionCSSWithSensitivity(".content", 3)); + } + + @Test + void testCreateIntelliIgnoreRegion() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + assertDoesNotThrow(() -> steps.iCreateIntelliIgnoreRegionCSS(".dynamic")); + } + + @Test + void testClearRegions() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + steps.iHaveAPercyInstance(); + steps.iCreateIgnoreRegionCSS(".ad"); + assertDoesNotThrow(steps::iClearPercyRegions); + } + + @Test + void testPercyShouldBeEnabledThrowsWithoutInit() { + PercySteps steps = new PercySteps(); + assertThrows(IllegalStateException.class, steps::percyShouldBeEnabled); + } + + @Test + void testPercyShouldBeEnabledSucceeds() { + PercySteps.setPage(mockPage); + PercySteps steps = new PercySteps(); + assertDoesNotThrow(steps::percyShouldBeEnabled); + } +} From 71853239d1e356d20b024d9fbb16db8ddb0e2eb1 Mon Sep 17 00:00:00 2001 From: Neha Sanserwal Date: Thu, 23 Apr 2026 10:19:19 +0530 Subject: [PATCH 2/5] Report Cucumber environment info in Percy build metadata - Add setClientInfo/setEnvironmentInfo to Percy and Environment classes - PercySteps sets client to "percy-cucumber-java-playwright/" and environment to "cucumber-java/; playwright-java" - Add Percy.getSdkVersion() public static method Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/io/percy/playwright/Environment.java | 21 +++++++++++++++++++ src/main/java/io/percy/playwright/Percy.java | 16 ++++++++++++++ .../percy/playwright/cucumber/PercySteps.java | 18 ++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/main/java/io/percy/playwright/Environment.java b/src/main/java/io/percy/playwright/Environment.java index 552373a..cfede57 100644 --- a/src/main/java/io/percy/playwright/Environment.java +++ b/src/main/java/io/percy/playwright/Environment.java @@ -9,12 +9,33 @@ class Environment { private final static String SDK_VERSION = "1.0.2"; private final static String SDK_NAME = "percy-playwright-java"; + private String clientInfoOverride; + private String environmentInfoOverride; + public String getClientInfo() { + if (clientInfoOverride != null) { + return clientInfoOverride; + } return SDK_NAME + "/" + SDK_VERSION; } public String getEnvironmentInfo() { + if (environmentInfoOverride != null) { + return environmentInfoOverride; + } String playwrightVersion = Playwright.class.getPackage().getImplementationVersion(); return String.format("playwright-java; %s", playwrightVersion); } + + void setClientInfo(String clientInfo) { + this.clientInfoOverride = clientInfo; + } + + void setEnvironmentInfo(String environmentInfo) { + this.environmentInfoOverride = environmentInfo; + } + + public static String getSdkVersion() { + return SDK_VERSION; + } } diff --git a/src/main/java/io/percy/playwright/Percy.java b/src/main/java/io/percy/playwright/Percy.java index 2f2224e..ff8b93e 100644 --- a/src/main/java/io/percy/playwright/Percy.java +++ b/src/main/java/io/percy/playwright/Percy.java @@ -73,6 +73,22 @@ public Percy(Page page) { this.env = new Environment(); } + /** + * Override the client info reported to Percy. + * Used by framework wrappers (e.g., Cucumber) to identify themselves. + */ + public void setClientInfo(String clientInfo, String environmentInfo) { + this.env.setClientInfo(clientInfo); + this.env.setEnvironmentInfo(environmentInfo); + } + + /** + * Get the SDK version string. + */ + public static String getSdkVersion() { + return Environment.getSdkVersion(); + } + /** * Creates a region configuration based on the provided parameters. * diff --git a/src/main/java/io/percy/playwright/cucumber/PercySteps.java b/src/main/java/io/percy/playwright/cucumber/PercySteps.java index 99b5c6f..e4238b7 100644 --- a/src/main/java/io/percy/playwright/cucumber/PercySteps.java +++ b/src/main/java/io/percy/playwright/cucumber/PercySteps.java @@ -71,6 +71,24 @@ public class PercySteps { public static void setPage(Page playwrightPage) { page = playwrightPage; percy = new Percy(page); + + // Identify as Cucumber wrapper in Percy build info + String sdkVersion = Percy.getSdkVersion(); + String cucumberVersion = getCucumberVersion(); + percy.setClientInfo( + "percy-cucumber-java-playwright/" + sdkVersion, + "cucumber-java/" + cucumberVersion + "; playwright-java" + ); + } + + private static String getCucumberVersion() { + try { + Package pkg = io.cucumber.java.en.Given.class.getPackage(); + String version = pkg != null ? pkg.getImplementationVersion() : null; + return version != null ? version : "unknown"; + } catch (Exception e) { + return "unknown"; + } } /** From 4b91f9e731dbe70ace37c4cc6d12298bbac5fa98 Mon Sep 17 00:00:00 2001 From: Neha Sanserwal Date: Thu, 23 Apr 2026 12:07:42 +0530 Subject: [PATCH 3/5] Add Percy Screenshot (Automate) steps to Cucumber support Add screenshot step definitions for BrowserStack Automate: - I take a Percy screenshot named {string} - I take a Percy screenshot named {string} with regions - I take a Percy screenshot named {string} with options: Co-Authored-By: Claude Opus 4.6 (1M context) --- .../percy/playwright/cucumber/PercySteps.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/java/io/percy/playwright/cucumber/PercySteps.java b/src/main/java/io/percy/playwright/cucumber/PercySteps.java index e4238b7..65649de 100644 --- a/src/main/java/io/percy/playwright/cucumber/PercySteps.java +++ b/src/main/java/io/percy/playwright/cucumber/PercySteps.java @@ -281,6 +281,47 @@ public void iTakeSnapshotWithOptions(String name, Map optionsTab percy.snapshot(name, options); } + // ------------------------------------------------------------------ + // When steps - Screenshot (Automate) + // ------------------------------------------------------------------ + + @When("I take a Percy screenshot named {string}") + public void iTakeScreenshot(String name) { + try { + percy.screenshot(name); + } catch (Exception e) { + throw new RuntimeException("Percy screenshot failed: " + e.getMessage(), e); + } + } + + @When("I take a Percy screenshot named {string} with regions") + public void iTakeScreenshotWithRegions(String name) { + Map options = new HashMap<>(); + if (!regions.isEmpty()) { + options.put("regions", new ArrayList<>(regions)); + regions.clear(); + } + try { + percy.screenshot(name, options); + } catch (Exception e) { + throw new RuntimeException("Percy screenshot failed: " + e.getMessage(), e); + } + } + + @When("I take a Percy screenshot named {string} with options:") + public void iTakeScreenshotWithOptions(String name, Map optionsTable) { + Map options = buildOptions(optionsTable); + if (!regions.isEmpty()) { + options.put("regions", new ArrayList<>(regions)); + regions.clear(); + } + try { + percy.screenshot(name, options); + } catch (Exception e) { + throw new RuntimeException("Percy screenshot failed: " + e.getMessage(), e); + } + } + // ------------------------------------------------------------------ // Then steps // ------------------------------------------------------------------ From 9efb2ead69ace6d10146bca361837cef5c8137bb Mon Sep 17 00:00:00 2001 From: Neha Sanserwal Date: Thu, 23 Apr 2026 12:20:44 +0530 Subject: [PATCH 4/5] Add parity with Robot Framework: missing region and snapshot steps - Add ignore/consider region with padding - Add consider region with XPath (+ diff sensitivity) - Add intelliignore region with XPath - Add snapshot steps: Shadow DOM disabled, responsive capture, sync - Add scopeOptions and sync parsing in buildOptions helper Co-Authored-By: Claude Opus 4.6 (1M context) --- .../percy/playwright/cucumber/PercySteps.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/main/java/io/percy/playwright/cucumber/PercySteps.java b/src/main/java/io/percy/playwright/cucumber/PercySteps.java index 65649de..db59ee6 100644 --- a/src/main/java/io/percy/playwright/cucumber/PercySteps.java +++ b/src/main/java/io/percy/playwright/cucumber/PercySteps.java @@ -180,6 +180,49 @@ public void iCreateIntelliIgnoreRegionCSS(String cssSelector) { regions.add(percy.createRegion(params)); } + @Given("I create a Percy ignore region with CSS selector {string} and padding {int}") + public void iCreateIgnoreRegionCSSWithPadding(String cssSelector, int padding) { + Map params = new HashMap<>(); + params.put("algorithm", "ignore"); + params.put("elementCSS", cssSelector); + params.put("padding", padding); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy ignore region with XPath {string} and padding {int}") + public void iCreateIgnoreRegionXPathWithPadding(String xpath, int padding) { + Map params = new HashMap<>(); + params.put("algorithm", "ignore"); + params.put("elementXpath", xpath); + params.put("padding", padding); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy consider region with XPath {string}") + public void iCreateConsiderRegionXPath(String xpath) { + Map params = new HashMap<>(); + params.put("algorithm", "standard"); + params.put("elementXpath", xpath); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy consider region with XPath {string} and diff sensitivity {int}") + public void iCreateConsiderRegionXPathWithSensitivity(String xpath, int sensitivity) { + Map params = new HashMap<>(); + params.put("algorithm", "standard"); + params.put("elementXpath", xpath); + params.put("diffSensitivity", sensitivity); + regions.add(percy.createRegion(params)); + } + + @Given("I create a Percy intelliignore region with XPath {string}") + public void iCreateIntelliIgnoreRegionXPath(String xpath) { + Map params = new HashMap<>(); + params.put("algorithm", "intelliignore"); + params.put("elementXpath", xpath); + regions.add(percy.createRegion(params)); + } + @Given("I clear Percy regions") public void iClearPercyRegions() { regions.clear(); @@ -250,6 +293,27 @@ public void iTakeSnapshotWithTestCase(String name, String testCase) { percy.snapshot(name, options); } + @When("I take a Percy snapshot named {string} with Shadow DOM disabled") + public void iTakeSnapshotWithShadowDomDisabled(String name) { + Map options = new HashMap<>(); + options.put("disableShadowDom", true); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with responsive capture") + public void iTakeSnapshotWithResponsiveCapture(String name) { + Map options = new HashMap<>(); + options.put("responsiveSnapshotCapture", true); + percy.snapshot(name, options); + } + + @When("I take a Percy snapshot named {string} with sync") + public void iTakeSnapshotWithSync(String name) { + Map options = new HashMap<>(); + options.put("sync", true); + percy.snapshot(name, options); + } + @When("I take a Percy snapshot named {string} with regions") public void iTakeSnapshotWithRegions(String name) { Map options = new HashMap<>(); @@ -361,8 +425,12 @@ private static Map buildOptions(Map table) { case "enableLayout": case "disableShadowDom": case "responsiveSnapshotCapture": + case "sync": options.put(key, Boolean.parseBoolean(value)); break; + case "scopeOptions": + options.put(key, new org.json.JSONObject(value).toMap()); + break; default: options.put(key, value); break; From 9e253e8cf4cbb13955c1d144e2a919797fa87130 Mon Sep 17 00:00:00 2001 From: Neha Sanserwal Date: Thu, 7 May 2026 09:46:03 +0530 Subject: [PATCH 5/5] Add snapshot and screenshot tests for Cucumber PercySteps The existing tests only covered lifecycle and region creation but never invoked percy.snapshot() or percy.screenshot(). This adds tests for every @When step method using a mocked Percy instance injected via reflection, verifying that each Cucumber step correctly delegates to the Percy SDK with the right name and options. Co-Authored-By: Claude Opus 4.6 --- .../playwright/cucumber/PercyStepsTest.java | 376 +++++++++++++++++- 1 file changed, 358 insertions(+), 18 deletions(-) diff --git a/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java b/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java index 89225a3..f42a4bc 100644 --- a/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java +++ b/src/test/java/io/percy/playwright/cucumber/PercyStepsTest.java @@ -8,14 +8,22 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.lang.reflect.Field; +import java.util.*; class PercyStepsTest { private Page mockPage; + private Percy mockPercy; + private PercySteps steps; @BeforeEach void setUp() { mockPage = mock(Page.class); + mockPercy = mock(Percy.class); + steps = new PercySteps(); } @AfterEach @@ -23,6 +31,25 @@ void tearDown() { PercySteps.reset(); } + private void initWithMockPercy() { + PercySteps.setPage(mockPage); + setPercyField(mockPercy); + } + + private void setPercyField(Percy percy) { + try { + Field field = PercySteps.class.getDeclaredField("percy"); + field.setAccessible(true); + field.set(null, percy); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // ------------------------------------------------------------------ + // Lifecycle tests + // ------------------------------------------------------------------ + @Test void testSetPageAndGetPercy() { PercySteps.setPage(mockPage); @@ -40,85 +67,398 @@ void testResetClearsState() { @Test void testIHaveAPercyInstanceThrowsWithoutPage() { - PercySteps steps = new PercySteps(); assertThrows(IllegalStateException.class, steps::iHaveAPercyInstance); } @Test void testIHaveAPercyInstanceSucceedsWithPage() { PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); assertDoesNotThrow(steps::iHaveAPercyInstance); } + // ------------------------------------------------------------------ + // Region creation tests + // ------------------------------------------------------------------ + @Test void testCreateIgnoreRegionCSS() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); assertDoesNotThrow(() -> steps.iCreateIgnoreRegionCSS(".ad-banner")); + verify(mockPercy).createRegion(argThat(map -> + "ignore".equals(map.get("algorithm")) && ".ad-banner".equals(map.get("elementCSS")) + )); } @Test void testCreateIgnoreRegionXPath() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); assertDoesNotThrow(() -> steps.iCreateIgnoreRegionXPath("//div[@id='header']")); + verify(mockPercy).createRegion(argThat(map -> + "ignore".equals(map.get("algorithm")) && "//div[@id='header']".equals(map.get("elementXpath")) + )); } @Test void testCreateIgnoreRegionBoundingBox() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); assertDoesNotThrow(() -> steps.iCreateIgnoreRegionBoundingBox(0, 0, 200, 100)); + verify(mockPercy).createRegion(argThat(map -> + "ignore".equals(map.get("algorithm")) && map.containsKey("boundingBox") + )); } @Test void testCreateConsiderRegionCSS() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); assertDoesNotThrow(() -> steps.iCreateConsiderRegionCSS(".content")); + verify(mockPercy).createRegion(argThat(map -> + "standard".equals(map.get("algorithm")) && ".content".equals(map.get("elementCSS")) + )); } @Test void testCreateConsiderRegionWithSensitivity() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); assertDoesNotThrow( () -> steps.iCreateConsiderRegionCSSWithSensitivity(".content", 3)); + verify(mockPercy).createRegion(argThat(map -> + "standard".equals(map.get("algorithm")) && Integer.valueOf(3).equals(map.get("diffSensitivity")) + )); } @Test void testCreateIntelliIgnoreRegion() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); assertDoesNotThrow(() -> steps.iCreateIntelliIgnoreRegionCSS(".dynamic")); + verify(mockPercy).createRegion(argThat(map -> + "intelliignore".equals(map.get("algorithm")) && ".dynamic".equals(map.get("elementCSS")) + )); + } + + @Test + void testCreateIgnoreRegionCSSWithPadding() { + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); + steps.iHaveAPercyInstance(); + steps.iCreateIgnoreRegionCSSWithPadding(".ad", 10); + verify(mockPercy).createRegion(argThat(map -> + "ignore".equals(map.get("algorithm")) + && ".ad".equals(map.get("elementCSS")) + && Integer.valueOf(10).equals(map.get("padding")) + )); + } + + @Test + void testCreateIgnoreRegionXPathWithPadding() { + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); + steps.iHaveAPercyInstance(); + steps.iCreateIgnoreRegionXPathWithPadding("//div", 5); + verify(mockPercy).createRegion(argThat(map -> + "ignore".equals(map.get("algorithm")) + && "//div".equals(map.get("elementXpath")) + && Integer.valueOf(5).equals(map.get("padding")) + )); + } + + @Test + void testCreateConsiderRegionXPath() { + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); + steps.iHaveAPercyInstance(); + steps.iCreateConsiderRegionXPath("//main"); + verify(mockPercy).createRegion(argThat(map -> + "standard".equals(map.get("algorithm")) && "//main".equals(map.get("elementXpath")) + )); + } + + @Test + void testCreateConsiderRegionXPathWithSensitivity() { + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); + steps.iHaveAPercyInstance(); + steps.iCreateConsiderRegionXPathWithSensitivity("//main", 5); + verify(mockPercy).createRegion(argThat(map -> + "standard".equals(map.get("algorithm")) + && "//main".equals(map.get("elementXpath")) + && Integer.valueOf(5).equals(map.get("diffSensitivity")) + )); + } + + @Test + void testCreateIntelliIgnoreRegionXPath() { + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); + steps.iHaveAPercyInstance(); + steps.iCreateIntelliIgnoreRegionXPath("//aside"); + verify(mockPercy).createRegion(argThat(map -> + "intelliignore".equals(map.get("algorithm")) && "//aside".equals(map.get("elementXpath")) + )); } @Test void testClearRegions() { - PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); + initWithMockPercy(); + when(mockPercy.createRegion(anyMap())).thenReturn(new HashMap<>()); steps.iHaveAPercyInstance(); steps.iCreateIgnoreRegionCSS(".ad"); assertDoesNotThrow(steps::iClearPercyRegions); } + // ------------------------------------------------------------------ + // Snapshot tests + // ------------------------------------------------------------------ + + @Test + void testTakeSnapshot() { + initWithMockPercy(); + steps.iTakeSnapshot("Homepage"); + verify(mockPercy).snapshot("Homepage"); + } + + @Test + void testTakeSnapshotWithWidths() { + initWithMockPercy(); + steps.iTakeSnapshotWithWidths("Responsive", "375,768,1280"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Responsive"), captor.capture()); + List widths = (List) captor.getValue().get("widths"); + assertEquals(Arrays.asList(375, 768, 1280), widths); + } + + @Test + void testTakeSnapshotWithMinHeight() { + initWithMockPercy(); + steps.iTakeSnapshotWithMinHeight("Tall Page", 2000); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Tall Page"), captor.capture()); + assertEquals(2000, captor.getValue().get("minHeight")); + } + + @Test + void testTakeSnapshotWithCSS() { + initWithMockPercy(); + steps.iTakeSnapshotWithCSS("Styled", "body { background: purple; }"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Styled"), captor.capture()); + assertEquals("body { background: purple; }", captor.getValue().get("percyCSS")); + } + + @Test + void testTakeSnapshotWithScope() { + initWithMockPercy(); + steps.iTakeSnapshotWithScope("Scoped", "#main"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Scoped"), captor.capture()); + assertEquals("#main", captor.getValue().get("scope")); + } + + @Test + void testTakeSnapshotWithLayout() { + initWithMockPercy(); + steps.iTakeSnapshotWithLayout("Layout Mode"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Layout Mode"), captor.capture()); + assertEquals(true, captor.getValue().get("enableLayout")); + } + + @Test + void testTakeSnapshotWithJS() { + initWithMockPercy(); + steps.iTakeSnapshotWithJS("JS Enabled"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("JS Enabled"), captor.capture()); + assertEquals(true, captor.getValue().get("enableJavaScript")); + } + + @Test + void testTakeSnapshotWithLabels() { + initWithMockPercy(); + steps.iTakeSnapshotWithLabels("Labeled", "regression,smoke"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Labeled"), captor.capture()); + assertEquals("regression,smoke", captor.getValue().get("labels")); + } + + @Test + void testTakeSnapshotWithTestCase() { + initWithMockPercy(); + steps.iTakeSnapshotWithTestCase("TC Snap", "TC-001"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("TC Snap"), captor.capture()); + assertEquals("TC-001", captor.getValue().get("testCase")); + } + + @Test + void testTakeSnapshotWithShadowDomDisabled() { + initWithMockPercy(); + steps.iTakeSnapshotWithShadowDomDisabled("No Shadow"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("No Shadow"), captor.capture()); + assertEquals(true, captor.getValue().get("disableShadowDom")); + } + + @Test + void testTakeSnapshotWithResponsiveCapture() { + initWithMockPercy(); + steps.iTakeSnapshotWithResponsiveCapture("Responsive Capture"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Responsive Capture"), captor.capture()); + assertEquals(true, captor.getValue().get("responsiveSnapshotCapture")); + } + + @Test + void testTakeSnapshotWithSync() { + initWithMockPercy(); + steps.iTakeSnapshotWithSync("Sync Snap"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Sync Snap"), captor.capture()); + assertEquals(true, captor.getValue().get("sync")); + } + + @Test + void testTakeSnapshotWithRegions() { + initWithMockPercy(); + Map fakeRegion = new HashMap<>(); + fakeRegion.put("algorithm", "ignore"); + when(mockPercy.createRegion(anyMap())).thenReturn(fakeRegion); + + steps.iHaveAPercyInstance(); + steps.iCreateIgnoreRegionCSS(".ad-banner"); + steps.iTakeSnapshotWithRegions("No Ads"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("No Ads"), captor.capture()); + List> regions = (List>) captor.getValue().get("regions"); + assertNotNull(regions); + assertEquals(1, regions.size()); + assertEquals("ignore", regions.get(0).get("algorithm")); + } + + @Test + void testTakeSnapshotWithRegionsClearsAfterSnapshot() { + initWithMockPercy(); + Map fakeRegion = new HashMap<>(); + fakeRegion.put("algorithm", "ignore"); + when(mockPercy.createRegion(anyMap())).thenReturn(fakeRegion); + + steps.iHaveAPercyInstance(); + steps.iCreateIgnoreRegionCSS(".ad"); + steps.iTakeSnapshotWithRegions("First"); + + steps.iTakeSnapshotWithRegions("Second"); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Second"), captor.capture()); + Map opts = captor.getValue(); + assertFalse(opts.containsKey("regions")); + } + + @Test + void testTakeSnapshotWithWidthsAndRegions() { + initWithMockPercy(); + Map fakeRegion = new HashMap<>(); + fakeRegion.put("algorithm", "standard"); + when(mockPercy.createRegion(anyMap())).thenReturn(fakeRegion); + + steps.iHaveAPercyInstance(); + steps.iCreateConsiderRegionCSS(".main"); + steps.iTakeSnapshotWithWidthsAndRegions("Wide+Regions", "768,1280"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("Wide+Regions"), captor.capture()); + Map opts = captor.getValue(); + assertEquals(Arrays.asList(768, 1280), opts.get("widths")); + assertNotNull(opts.get("regions")); + } + + @Test + void testTakeSnapshotWithOptionsTable() { + initWithMockPercy(); + Map optionsTable = new LinkedHashMap<>(); + optionsTable.put("widths", "375,1280"); + optionsTable.put("minHeight", "2000"); + optionsTable.put("percyCSS", "body { color: red; }"); + optionsTable.put("enableJavaScript", "true"); + optionsTable.put("sync", "true"); + + steps.iTakeSnapshotWithOptions("With Options", optionsTable); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).snapshot(eq("With Options"), captor.capture()); + Map opts = captor.getValue(); + assertEquals(Arrays.asList(375, 1280), opts.get("widths")); + assertEquals(2000, opts.get("minHeight")); + assertEquals("body { color: red; }", opts.get("percyCSS")); + assertEquals(true, opts.get("enableJavaScript")); + assertEquals(true, opts.get("sync")); + } + + // ------------------------------------------------------------------ + // Screenshot (Automate) tests + // ------------------------------------------------------------------ + + @Test + void testTakeScreenshot() throws Exception { + initWithMockPercy(); + steps.iTakeScreenshot("Login Page"); + verify(mockPercy).screenshot("Login Page"); + } + + @Test + void testTakeScreenshotWithRegions() throws Exception { + initWithMockPercy(); + Map fakeRegion = new HashMap<>(); + fakeRegion.put("algorithm", "ignore"); + when(mockPercy.createRegion(anyMap())).thenReturn(fakeRegion); + + steps.iHaveAPercyInstance(); + steps.iCreateIgnoreRegionCSS(".banner"); + steps.iTakeScreenshotWithRegions("No Banner"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).screenshot(eq("No Banner"), captor.capture()); + List> regions = (List>) captor.getValue().get("regions"); + assertNotNull(regions); + assertEquals(1, regions.size()); + } + + @Test + void testTakeScreenshotWithOptions() throws Exception { + initWithMockPercy(); + Map optionsTable = new LinkedHashMap<>(); + optionsTable.put("percyCSS", "body { color: blue; }"); + + steps.iTakeScreenshotWithOptions("Styled Screenshot", optionsTable); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(mockPercy).screenshot(eq("Styled Screenshot"), captor.capture()); + assertEquals("body { color: blue; }", captor.getValue().get("percyCSS")); + } + + // ------------------------------------------------------------------ + // Then step tests + // ------------------------------------------------------------------ + @Test void testPercyShouldBeEnabledThrowsWithoutInit() { - PercySteps steps = new PercySteps(); assertThrows(IllegalStateException.class, steps::percyShouldBeEnabled); } @Test void testPercyShouldBeEnabledSucceeds() { PercySteps.setPage(mockPage); - PercySteps steps = new PercySteps(); assertDoesNotThrow(steps::percyShouldBeEnabled); } }