Skip to content

Commit 03e6684

Browse files
test: JaCoCo coverage gate + raise coverage ~33% → ~85% (#315)
* test: add JaCoCo coverage + 83% line gate, raise coverage to ~85% The SDK had no coverage instrumentation. The existing SdkTest aborts at @BeforeAll without a live Firefox, so almost no logic was measured (~33% floor). This adds JaCoCo (prepare-agent + report + check) and a new mock/HTTP-based PercyLogicTest (43 tests) that covers the SDK logic without a browser, lifting non-driver line coverage to ~84.7% (the CI browser run reaches ~87.8%). Gate: jacoco:check bound to the test phase, LINE minimum 0.83 — the honest floor for the unit-testable logic. The remaining uncovered lines are live-WebDriver-only (the JS-execution/cookie body of snapshot(), changeWindowDimensionAndWait CDP/resize, processFrame iframe recovery, the responsive-capture sleep) and are exercised by SdkTest on the browser CI (MOZ_HEADLESS=1 npx percy exec -- mvn test). PercyLogicTest covers: createRegion variants, snapshot/screenshot dispatch + guards, web DOM post + automate session metadata + region element conversion, responsive width-config HTTP, readiness config + waitForReady disabled paths, getSerializedDOM (cookies, cross-origin iframe, readiness diagnostics), healthcheck branches, log, Environment, setClientInfo, DriverMetadata + Cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: make percy-disabled and responsive-height tests CI-deterministic The new PercyLogicTest cases passed locally (no Percy CLI) but failed on CI where `percy exec` runs a live CLI: - screenshotReturnsNullWhenPercyDisabled relied on isPercyEnabled defaulting to false; under percy exec the healthcheck enables it. Force isPercyEnabled =false via reflection so the disabled-path assertion is deterministic. - resolveResponsiveTargetHeightHonoursFeatureFlag's no-minHeight branch read cliConfig.snapshot.minHeight (1024 on CI). Clear cliConfig so it falls back to currentHeight as intended. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: raise line coverage to ~99% with mock-based tests + green JaCoCo gate Brings the deterministic (non-live-browser) suite to 742/747 = ~99.3% line coverage and sets the JaCoCo BUNDLE LINE floor to a green 0.94 so the Linux CI Test jobs pass with headroom for macOS<->Linux counting variance. New mock/HTTP-based coverage (no exclusions, no @generated, no pragmas): - PercyDriverPathTest: WebDriver/JavascriptExecutor/HttpServer-mocked tests for snapshot() cookie/responsive/exception paths, healthcheck, fetchPercyDOM, getResponsiveWidths, waitForReady timeout set/restore, resolveReadinessConfig, CORS-iframe / FatalIframe handling, captureResponsiveDom CDP + setSize fallback + resize-wait + non-numeric-sleep / null-resizeCount paths. - CacheTest: Cache default ctor; DriverMetadata TracedCommandExecutor unwrap (success + reflective-failure fallback). - PercyStepsTest + a behavior-preserving resolveCucumberVersion(Callable) seam in PercySteps so the version null/throw fallbacks are testable without a jar manifest; lazy-Percy, options-table regions and scopeOptions parsing. The five remaining uncovered Percy lines are behaviorally unreachable defensive branches (null x-percy-core-version header Apache HttpClient never yields, the ChromeDriver no-CDP reflection fallback, and a non-numeric width the widths-config parser cannot produce) and were not covered to avoid altering production behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bf3f565 commit 03e6684

7 files changed

Lines changed: 2088 additions & 2 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ node_modules/
1212
.vscode
1313
.settings
1414
.DS_Store
15+
16+
# bstack-ai-harness:begin (managed — do not edit between markers)
17+
bstack-ai-harness.yml
18+
.harness-docs.json
19+
.harness-manifest.json
20+
CLAUDE.md
21+
.claude/
22+
# bstack-ai-harness:end

pom.xml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,58 @@
184184
<useSystemClassLoader>false</useSystemClassLoader>
185185
</configuration>
186186
</plugin>
187+
<plugin>
188+
<groupId>org.jacoco</groupId>
189+
<artifactId>jacoco-maven-plugin</artifactId>
190+
<version>0.8.12</version>
191+
<executions>
192+
<execution>
193+
<id>prepare-agent</id>
194+
<goals>
195+
<goal>prepare-agent</goal>
196+
</goals>
197+
</execution>
198+
<execution>
199+
<id>report</id>
200+
<phase>test</phase>
201+
<goals>
202+
<goal>report</goal>
203+
</goals>
204+
</execution>
205+
<execution>
206+
<id>check</id>
207+
<phase>test</phase>
208+
<goals>
209+
<goal>check</goal>
210+
</goals>
211+
<configuration>
212+
<rules>
213+
<rule>
214+
<element>BUNDLE</element>
215+
<limits>
216+
<limit>
217+
<counter>LINE</counter>
218+
<value>COVEREDRATIO</value>
219+
<!-- Green floor for the deterministic mock/HTTP-covered
220+
logic. Locally (macOS, without the live-browser
221+
SdkTest) the suite covers 742/747 = ~0.9933 lines;
222+
the only five misses are behaviorally unreachable
223+
defensive branches (a null core-version header that
224+
Apache HttpClient never yields, the ChromeDriver
225+
no-CDP reflection fallback, and a non-numeric width
226+
that the widths-config parser cannot produce). The
227+
floor is set ~0.05 below the achieved ratio to absorb
228+
macOS<->Linux JaCoCo counting variance so the Linux
229+
CI Test jobs stay green. -->
230+
<minimum>0.94</minimum>
231+
</limit>
232+
</limits>
233+
</rule>
234+
</rules>
235+
</configuration>
236+
</execution>
237+
</executions>
238+
</plugin>
187239
<plugin>
188240
<artifactId>maven-jar-plugin</artifactId>
189241
<version>3.3.0</version>

src/main/java/io/percy/selenium/cucumber/PercySteps.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,25 @@ public static void setDriver(WebDriver webDriver) {
8585
}
8686

8787
private static String getCucumberVersion() {
88-
try {
88+
// The version lookup is delegated to resolveCucumberVersion so the
89+
// null / throwing fallbacks can be exercised deterministically in tests
90+
// (the cucumber jar manifest is absent under test). Behavior is identical
91+
// to reading io.cucumber.java.en.Given's implementation version inline.
92+
return resolveCucumberVersion(() -> {
8993
Package pkg = io.cucumber.java.en.Given.class.getPackage();
90-
String version = pkg != null ? pkg.getImplementationVersion() : null;
94+
return pkg != null ? pkg.getImplementationVersion() : null;
95+
});
96+
}
97+
98+
/**
99+
* Resolves the cucumber version using the supplied {@code resolver},
100+
* falling back to {@code "unknown"} when the resolver returns null or
101+
* throws. Package-private seam so the fallback branches are testable
102+
* without a manifest; not part of the public API.
103+
*/
104+
static String resolveCucumberVersion(java.util.concurrent.Callable<String> resolver) {
105+
try {
106+
String version = resolver.call();
91107
return version != null ? version : "unknown";
92108
} catch (Exception e) {
93109
return "unknown";

src/test/java/io/percy/selenium/CacheTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static org.mockito.Mockito.*;
1414
import java.net.URL;
1515
import java.util.concurrent.ConcurrentHashMap;
16+
import java.lang.reflect.Field;
1617

1718
public class CacheTest {
1819
private static RemoteWebDriver mockedDriver;
@@ -58,4 +59,70 @@ public void testCommandExecutorUrl() {
5859
String commandExecutorUrl = driverMetadata.getCommandExecutorUrl();
5960
assertEquals(Cache.CACHE_MAP.get(key), commandExecutorUrl);
6061
}
62+
63+
@Test
64+
public void testCacheInstantiable() {
65+
// Exercises the implicit default constructor of Cache (its only line).
66+
assertNotNull(new Cache());
67+
}
68+
69+
// ------------------------------------------------------------------
70+
// getCommandExecutorUrl: TracedCommandExecutor unwrap branch.
71+
//
72+
// When the executor's class name contains "TracedCommandExecutor",
73+
// DriverMetadata reflectively reads its private `delegate` field and
74+
// unwraps to the underlying HttpCommandExecutor. These fixtures let us
75+
// drive both the successful unwrap and the reflective-failure fallback
76+
// without a live Selenium tracing executor.
77+
// ------------------------------------------------------------------
78+
79+
/** Mirrors Selenium's internal wrapper: a delegate field holding the real executor. */
80+
static class TracedCommandExecutor implements CommandExecutor {
81+
@SuppressWarnings("unused")
82+
private final CommandExecutor delegate;
83+
TracedCommandExecutor(CommandExecutor delegate) { this.delegate = delegate; }
84+
@Override
85+
public org.openqa.selenium.remote.Response execute(org.openqa.selenium.remote.Command command) {
86+
throw new UnsupportedOperationException();
87+
}
88+
}
89+
90+
/** Same name suffix but without a `delegate` field, to drive the catch fallback. */
91+
static class BrokenTracedCommandExecutor implements CommandExecutor {
92+
@Override
93+
public org.openqa.selenium.remote.Response execute(org.openqa.selenium.remote.Command command) {
94+
throw new UnsupportedOperationException();
95+
}
96+
}
97+
98+
@Test
99+
public void testCommandExecutorUrlUnwrapsTracedExecutor() throws Exception {
100+
Cache.CACHE_MAP.clear();
101+
RemoteWebDriver driver = mock(RemoteWebDriver.class);
102+
when(driver.getSessionId()).thenReturn(new SessionId("traced-1"));
103+
104+
HttpCommandExecutor inner = mock(HttpCommandExecutor.class);
105+
when(inner.getAddressOfRemoteServer()).thenReturn(new URL("https://hub.example.com/wd/hub"));
106+
TracedCommandExecutor traced = new TracedCommandExecutor(inner);
107+
when(driver.getCommandExecutor()).thenReturn(traced);
108+
109+
DriverMetadata driverMetadata = new DriverMetadata(driver);
110+
String url = driverMetadata.getCommandExecutorUrl();
111+
assertEquals("https://hub.example.com/wd/hub", url);
112+
}
113+
114+
@Test
115+
public void testCommandExecutorUrlReturnsErrorWhenDelegateFieldMissing() {
116+
Cache.CACHE_MAP.clear();
117+
RemoteWebDriver driver = mock(RemoteWebDriver.class);
118+
when(driver.getSessionId()).thenReturn(new SessionId("traced-2"));
119+
when(driver.getCommandExecutor()).thenReturn(new BrokenTracedCommandExecutor());
120+
121+
DriverMetadata driverMetadata = new DriverMetadata(driver);
122+
// No `delegate` field -> reflective lookup throws and the catch returns
123+
// the exception's string form rather than a URL.
124+
String result = driverMetadata.getCommandExecutorUrl();
125+
assertNotNull(result);
126+
assertTrue(result.contains("NoSuchFieldException") || result.contains("delegate"));
127+
}
61128
}

0 commit comments

Comments
 (0)