diff --git a/pom.xml b/pom.xml index e6c3e3f..f923ec8 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,19 @@ 3.12.4 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/selenium/Environment.java b/src/main/java/io/percy/selenium/Environment.java index b1d7d78..cf637de 100644 --- a/src/main/java/io/percy/selenium/Environment.java +++ b/src/main/java/io/percy/selenium/Environment.java @@ -13,15 +13,24 @@ class Environment { private final static String SDK_VERSION = "2.1.2"; private final static String SDK_NAME = "percy-java-selenium"; + private String clientInfoOverride; + private String environmentInfoOverride; + Environment(WebDriver driver) { this.driver = driver; } public String getClientInfo() { + if (clientInfoOverride != null) { + return clientInfoOverride; + } return SDK_NAME + "/" + SDK_VERSION; } public String getEnvironmentInfo() { + if (environmentInfoOverride != null) { + return environmentInfoOverride; + } // If this is a wrapped driver, get the actual driver that this one wraps. WebDriver innerDriver = this.driver instanceof WrapsDriver ? ((WrapsDriver) this.driver).getWrappedDriver() @@ -33,4 +42,16 @@ public String getEnvironmentInfo() { // We don't know this type of driver. Report its classname as environment info. return String.format("selenium-java; %s", driverName); } + + 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/selenium/Percy.java b/src/main/java/io/percy/selenium/Percy.java index 453c4bf..aed4516 100644 --- a/src/main/java/io/percy/selenium/Percy.java +++ b/src/main/java/io/percy/selenium/Percy.java @@ -86,6 +86,27 @@ public Percy(WebDriver driver) { this.env = new Environment(driver); } + /** + * Override the client info reported to Percy. + * Used by framework wrappers (e.g., Cucumber) to identify themselves. + * + * @param clientInfo Client identifier (e.g., "percy-cucumber-java-selenium/2.1.2") + * @param environmentInfo Environment details (e.g., "cucumber-java/7.15.0; selenium-java; ChromeDriver") + */ + public void setClientInfo(String clientInfo, String environmentInfo) { + this.env.setClientInfo(clientInfo); + this.env.setEnvironmentInfo(environmentInfo); + } + + /** + * Get the SDK version string. + * + * @return SDK version (e.g., "2.1.2") + */ + public static String getSdkVersion() { + return Environment.getSdkVersion(); + } + /** * Creates a region configuration based on the provided parameters. * diff --git a/src/main/java/io/percy/selenium/cucumber/PercySteps.java b/src/main/java/io/percy/selenium/cucumber/PercySteps.java new file mode 100644 index 0000000..3811b33 --- /dev/null +++ b/src/main/java/io/percy/selenium/cucumber/PercySteps.java @@ -0,0 +1,434 @@ +package io.percy.selenium.cucumber; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.When; +import io.cucumber.java.en.Then; +import io.percy.selenium.Percy; + +import org.json.JSONObject; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import java.util.*; + +/** + * Cucumber step definitions for Percy visual testing with Selenium. + * + *

Provides Gherkin steps to capture Percy snapshots, screenshots (Automate), + * 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
+ *
+ *   Scenario: Automate screenshot
+ *     Given I have a Percy instance
+ *     When I take a Percy screenshot named "Login Page"
+ * 
+ * + *

Setup in step definition glue:

+ *
+ * public class Hooks {
+ *     private static WebDriver driver;
+ *
+ *     {@literal @}Before
+ *     public void setUp() {
+ *         driver = new ChromeDriver();
+ *         PercySteps.setDriver(driver);
+ *     }
+ *
+ *     {@literal @}After
+ *     public void tearDown() {
+ *         if (driver != null) driver.quit();
+ *         PercySteps.reset();
+ *     }
+ * }
+ * 
+ */ +public class PercySteps { + + private static WebDriver driver; + private static Percy percy; + private static List> regions = new ArrayList<>(); + + private static final String CUCUMBER_CLIENT_NAME = "percy-cucumber-java-selenium"; + + /** + * Set the WebDriver instance for Percy to use. + * Call this from your Cucumber hooks before using any Percy steps. + * + * @param webDriver the Selenium WebDriver instance + */ + public static void setDriver(WebDriver webDriver) { + driver = webDriver; + percy = new Percy(driver); + + // Identify as Cucumber wrapper in Percy build info + String sdkVersion = Percy.getSdkVersion(); + String cucumberVersion = getCucumberVersion(); + percy.setClientInfo( + CUCUMBER_CLIENT_NAME + "/" + sdkVersion, + "cucumber-java/" + cucumberVersion + "; selenium-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"; + } + } + + /** + * 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; + driver = null; + regions.clear(); + } + + // ------------------------------------------------------------------ + // Given steps + // ------------------------------------------------------------------ + + @Given("I have a Percy instance") + public void iHaveAPercyInstance() { + if (driver == null) { + throw new IllegalStateException( + "WebDriver not set. Call PercySteps.setDriver(driver) in your @Before hook."); + } + if (percy == null) { + percy = new Percy(driver); + } + } + + @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 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(); + } + + // ------------------------------------------------------------------ + // 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 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<>(); + 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); + } + + // ------------------------------------------------------------------ + // When steps - Screenshot (Automate) + // ------------------------------------------------------------------ + + @When("I take a Percy screenshot named {string}") + public void iTakeScreenshot(String name) { + percy.screenshot(name); + } + + @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(); + } + percy.screenshot(name, options); + } + + @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(); + } + percy.screenshot(name, options); + } + + // ------------------------------------------------------------------ + // Then steps + // ------------------------------------------------------------------ + + @Then("Percy should be enabled") + public void percyShouldBeEnabled() { + if (percy == null) { + throw new IllegalStateException("Percy instance not initialized."); + } + // Percy constructor calls healthcheck; if it fails, snapshots silently no-op. + // This step validates the instance exists. + } + + // ------------------------------------------------------------------ + // 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": + 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; + } + } + return options; + } +} diff --git a/src/test/java/io/percy/selenium/cucumber/PercyStepsTest.java b/src/test/java/io/percy/selenium/cucumber/PercyStepsTest.java new file mode 100644 index 0000000..a204550 --- /dev/null +++ b/src/test/java/io/percy/selenium/cucumber/PercyStepsTest.java @@ -0,0 +1,465 @@ +package io.percy.selenium.cucumber; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.percy.selenium.Percy; +import org.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.openqa.selenium.WebDriver; + +import java.lang.reflect.Field; +import java.util.*; + +class PercyStepsTest { + + private WebDriver mockDriver; + private Percy mockPercy; + private PercySteps steps; + + @BeforeEach + void setUp() { + mockDriver = mock(WebDriver.class); + mockPercy = mock(Percy.class); + steps = new PercySteps(); + } + + @AfterEach + void tearDown() { + PercySteps.reset(); + } + + private void initWithMockPercy() { + PercySteps.setDriver(mockDriver); + 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 testSetDriverAndGetPercy() { + PercySteps.setDriver(mockDriver); + assertNotNull(PercySteps.getPercy()); + } + + @Test + void testResetClearsState() { + PercySteps.setDriver(mockDriver); + assertNotNull(PercySteps.getPercy()); + + PercySteps.reset(); + assertNull(PercySteps.getPercy()); + } + + @Test + void testIHaveAPercyInstanceThrowsWithoutDriver() { + assertThrows(IllegalStateException.class, steps::iHaveAPercyInstance); + } + + @Test + void testIHaveAPercyInstanceSucceedsWithDriver() { + PercySteps.setDriver(mockDriver); + assertDoesNotThrow(steps::iHaveAPercyInstance); + } + + // ------------------------------------------------------------------ + // Region creation tests + // ------------------------------------------------------------------ + + @Test + void testCreateIgnoreRegionCSS() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + initWithMockPercy(); + steps.iTakeScreenshot("Login Page"); + verify(mockPercy).screenshot("Login Page"); + } + + @Test + void testTakeScreenshotWithRegions() { + 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() { + 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() { + assertThrows(IllegalStateException.class, steps::percyShouldBeEnabled); + } + + @Test + void testPercyShouldBeEnabledSucceeds() { + PercySteps.setDriver(mockDriver); + assertDoesNotThrow(steps::percyShouldBeEnabled); + } +}