Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ node_modules/
.vscode
.settings
.DS_Store

# bstack-ai-harness:begin (managed — do not edit between markers)
bstack-ai-harness.yml
.harness-docs.json
.harness-manifest.json
CLAUDE.md
.claude/
# bstack-ai-harness:end
52 changes: 52 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,58 @@
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>test</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<!-- Green floor for the deterministic mock/HTTP-covered
logic. Locally (macOS, without the live-browser
SdkTest) the suite covers 742/747 = ~0.9933 lines;
the only five misses are behaviorally unreachable
defensive branches (a null core-version header that
Apache HttpClient never yields, the ChromeDriver
no-CDP reflection fallback, and a non-numeric width
that the widths-config parser cannot produce). The
floor is set ~0.05 below the achieved ratio to absorb
macOS<->Linux JaCoCo counting variance so the Linux
CI Test jobs stay green. -->
<minimum>0.94</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/io/percy/selenium/cucumber/PercySteps.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,25 @@ public static void setDriver(WebDriver webDriver) {
}

private static String getCucumberVersion() {
try {
// The version lookup is delegated to resolveCucumberVersion so the
// null / throwing fallbacks can be exercised deterministically in tests
// (the cucumber jar manifest is absent under test). Behavior is identical
// to reading io.cucumber.java.en.Given's implementation version inline.
return resolveCucumberVersion(() -> {
Package pkg = io.cucumber.java.en.Given.class.getPackage();
String version = pkg != null ? pkg.getImplementationVersion() : null;
return pkg != null ? pkg.getImplementationVersion() : null;
});
}

/**
* Resolves the cucumber version using the supplied {@code resolver},
* falling back to {@code "unknown"} when the resolver returns null or
* throws. Package-private seam so the fallback branches are testable
* without a manifest; not part of the public API.
*/
static String resolveCucumberVersion(java.util.concurrent.Callable<String> resolver) {
try {
String version = resolver.call();
return version != null ? version : "unknown";
} catch (Exception e) {
return "unknown";
Expand Down
67 changes: 67 additions & 0 deletions src/test/java/io/percy/selenium/CacheTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.mockito.Mockito.*;
import java.net.URL;
import java.util.concurrent.ConcurrentHashMap;
import java.lang.reflect.Field;

public class CacheTest {
private static RemoteWebDriver mockedDriver;
Expand Down Expand Up @@ -58,4 +59,70 @@ public void testCommandExecutorUrl() {
String commandExecutorUrl = driverMetadata.getCommandExecutorUrl();
assertEquals(Cache.CACHE_MAP.get(key), commandExecutorUrl);
}

@Test
public void testCacheInstantiable() {
// Exercises the implicit default constructor of Cache (its only line).
assertNotNull(new Cache());
}

// ------------------------------------------------------------------
// getCommandExecutorUrl: TracedCommandExecutor unwrap branch.
//
// When the executor's class name contains "TracedCommandExecutor",
// DriverMetadata reflectively reads its private `delegate` field and
// unwraps to the underlying HttpCommandExecutor. These fixtures let us
// drive both the successful unwrap and the reflective-failure fallback
// without a live Selenium tracing executor.
// ------------------------------------------------------------------

/** Mirrors Selenium's internal wrapper: a delegate field holding the real executor. */
static class TracedCommandExecutor implements CommandExecutor {
@SuppressWarnings("unused")
private final CommandExecutor delegate;
TracedCommandExecutor(CommandExecutor delegate) { this.delegate = delegate; }
@Override
public org.openqa.selenium.remote.Response execute(org.openqa.selenium.remote.Command command) {
throw new UnsupportedOperationException();
}
}

/** Same name suffix but without a `delegate` field, to drive the catch fallback. */
static class BrokenTracedCommandExecutor implements CommandExecutor {
@Override
public org.openqa.selenium.remote.Response execute(org.openqa.selenium.remote.Command command) {
throw new UnsupportedOperationException();
}
}

@Test
public void testCommandExecutorUrlUnwrapsTracedExecutor() throws Exception {
Cache.CACHE_MAP.clear();
RemoteWebDriver driver = mock(RemoteWebDriver.class);
when(driver.getSessionId()).thenReturn(new SessionId("traced-1"));

HttpCommandExecutor inner = mock(HttpCommandExecutor.class);
when(inner.getAddressOfRemoteServer()).thenReturn(new URL("https://hub.example.com/wd/hub"));
TracedCommandExecutor traced = new TracedCommandExecutor(inner);
when(driver.getCommandExecutor()).thenReturn(traced);

DriverMetadata driverMetadata = new DriverMetadata(driver);
String url = driverMetadata.getCommandExecutorUrl();
assertEquals("https://hub.example.com/wd/hub", url);
}

@Test
public void testCommandExecutorUrlReturnsErrorWhenDelegateFieldMissing() {
Cache.CACHE_MAP.clear();
RemoteWebDriver driver = mock(RemoteWebDriver.class);
when(driver.getSessionId()).thenReturn(new SessionId("traced-2"));
when(driver.getCommandExecutor()).thenReturn(new BrokenTracedCommandExecutor());

DriverMetadata driverMetadata = new DriverMetadata(driver);
// No `delegate` field -> reflective lookup throws and the catch returns
// the exception's string form rather than a URL.
String result = driverMetadata.getCommandExecutorUrl();
assertNotNull(result);
assertTrue(result.contains("NoSuchFieldException") || result.contains("delegate"));
}
}
Loading
Loading