@@ -53,6 +53,7 @@ public class Percy {
5353 private static String PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE = System .getenv ().getOrDefault ("PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE" , "false" ).toLowerCase ();
5454
5555 private static boolean PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT = Boolean .parseBoolean (System .getenv ().getOrDefault ("PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT" , "false" ));
56+ private static final int WIDTHS_CONFIG_TIMEOUT_MS = 30000 ;
5657 // for logging
5758 private static String LABEL = "[\u001b [35m" + (PERCY_DEBUG ? "percy:java" : "percy" ) + "\u001b [39m]" ;
5859
@@ -252,51 +253,12 @@ public JSONObject snapshot(String name, @Nullable List<Integer> widths, Integer
252253 }
253254
254255 private List <Map <String , Object >> getResponsiveWidths (List <Integer > widths ) {
255- String queryParam = "" ;
256- if (widths != null && !widths .isEmpty ()) {
257- String joined = widths .stream ().map (String ::valueOf ).collect (Collectors .joining ("," ));
258- queryParam = "?widths=" + joined ;
259- }
260-
261- int timeout = 30000 ; // 30 seconds
262- RequestConfig requestConfig = RequestConfig .custom ()
263- .setSocketTimeout (timeout )
264- .setConnectTimeout (timeout )
265- .build ();
256+ String queryParam = buildWidthsQueryParam (widths );
257+ RequestConfig requestConfig = buildRequestConfig (WIDTHS_CONFIG_TIMEOUT_MS );
266258
267259 try (CloseableHttpClient httpClient = HttpClients .custom ().setDefaultRequestConfig (requestConfig ).build ()) {
268- HttpGet httpget = new HttpGet (PERCY_SERVER_ADDRESS + "/percy/widths-config" + queryParam );
269- HttpResponse response = httpClient .execute (httpget );
270- int statusCode = response .getStatusLine ().getStatusCode ();
271-
272- if (statusCode != 200 ) {
273- EntityUtils .consume (response .getEntity ());
274- log ("Update Percy CLI to the latest version to use responsiveSnapshotCapture" );
275- throw new RuntimeException (
276- "Failed to fetch widths-config (HTTP " + statusCode + ")" );
277- }
278-
279- String responseString = EntityUtils .toString (response .getEntity (), "UTF-8" );
280- JSONObject json = new JSONObject (responseString );
281-
282- if (!json .has ("widths" ) || json .isNull ("widths" )) {
283- log ("Update Percy CLI to the latest version to use responsiveSnapshotCapture" );
284- throw new RuntimeException (
285- "Missing \" widths\" in widths-config response" );
286- }
287-
288- JSONArray widthsArray = json .getJSONArray ("widths" );
289- List <Map <String , Object >> result = new ArrayList <>();
290- for (int i = 0 ; i < widthsArray .length (); i ++) {
291- JSONObject entry = widthsArray .getJSONObject (i );
292- Map <String , Object > item = new HashMap <>();
293- item .put ("width" , entry .getInt ("width" ));
294- if (entry .has ("height" ) && !entry .isNull ("height" )) {
295- item .put ("height" , entry .getInt ("height" ));
296- }
297- result .add (item );
298- }
299- return result ;
260+ HttpResponse response = fetchWidthsConfigResponse (httpClient , queryParam );
261+ return parseWidthsConfigResponse (response );
300262 } catch (RuntimeException re ) {
301263 throw re ;
302264 } catch (Exception ex ) {
@@ -306,6 +268,63 @@ private List<Map<String, Object>> getResponsiveWidths(List<Integer> widths) {
306268 "Failed to fetch widths-config: " + ex .getMessage (), ex );
307269 }
308270 }
271+
272+ // Builds the optional `?widths=` query string from SDK-provided widths.
273+ private String buildWidthsQueryParam (List <Integer > widths ) {
274+ if (widths == null || widths .isEmpty ()) {
275+ return "" ;
276+ }
277+ String joined = widths .stream ().map (String ::valueOf ).collect (Collectors .joining ("," ));
278+ return "?widths=" + joined ;
279+ }
280+
281+ // Creates HTTP request timeout configuration for the widths-config endpoint.
282+ private RequestConfig buildRequestConfig (int timeoutMs ) {
283+ return RequestConfig .custom ()
284+ .setSocketTimeout (timeoutMs )
285+ .setConnectTimeout (timeoutMs )
286+ .build ();
287+ }
288+
289+ // Calls Percy CLI widths-config endpoint and validates that the HTTP status is successful.
290+ private HttpResponse fetchWidthsConfigResponse (CloseableHttpClient httpClient , String queryParam ) throws Exception {
291+ HttpGet httpget = new HttpGet (PERCY_SERVER_ADDRESS + "/percy/widths-config" + queryParam );
292+ HttpResponse response = httpClient .execute (httpget );
293+ int statusCode = response .getStatusLine ().getStatusCode ();
294+
295+ if (statusCode != 200 ) {
296+ EntityUtils .consume (response .getEntity ());
297+ log ("Update Percy CLI to the latest version to use responsiveSnapshotCapture" );
298+ throw new RuntimeException ("Failed to fetch widths-config (HTTP " + statusCode + ")" );
299+ }
300+
301+ return response ;
302+ }
303+
304+ // Parses widths-config JSON and converts the payload to SDK width/height maps.
305+ private List <Map <String , Object >> parseWidthsConfigResponse (HttpResponse response ) throws Exception {
306+ String responseString = EntityUtils .toString (response .getEntity (), "UTF-8" );
307+ JSONObject json = new JSONObject (responseString );
308+
309+ if (!json .has ("widths" ) || json .isNull ("widths" )) {
310+ log ("Update Percy CLI to the latest version to use responsiveSnapshotCapture" );
311+ throw new RuntimeException ("Missing \" widths\" in widths-config response" );
312+ }
313+
314+ JSONArray widthsArray = json .getJSONArray ("widths" );
315+ List <Map <String , Object >> result = new ArrayList <>();
316+ for (int i = 0 ; i < widthsArray .length (); i ++) {
317+ JSONObject entry = widthsArray .getJSONObject (i );
318+ Map <String , Object > item = new HashMap <>();
319+ item .put ("width" , entry .getInt ("width" ));
320+ if (entry .has ("height" ) && !entry .isNull ("height" )) {
321+ item .put ("height" , entry .getInt ("height" ));
322+ }
323+ result .add (item );
324+ }
325+ return result ;
326+ }
327+
309328 private boolean isCaptureResponsiveDOM (Map <String , Object > options ) {
310329 if (cliConfig .has ("percy" ) && !cliConfig .isNull ("percy" )) {
311330 JSONObject percyProperty = cliConfig .getJSONObject ("percy" );
@@ -785,6 +804,53 @@ private List<Integer> extractResponsiveWidths(Map<String, Object> options) {
785804 return coercedWidths .isEmpty () ? null : coercedWidths ;
786805 }
787806
807+ // Resolves final viewport height for responsive capture using minHeight config when enabled.
808+ private int resolveResponsiveTargetHeight (Map <String , Object > options , JavascriptExecutor jse , int currentHeight ) {
809+ if (!PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT ) {
810+ log ("PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT is disabled, using current window height: " + currentHeight , "debug" );
811+ return currentHeight ;
812+ }
813+
814+ Integer minHeight = resolveConfiguredMinHeight (options );
815+ if (minHeight == null ) {
816+ log ("minHeight not found in options or cliConfig, using current window height: " + currentHeight , "debug" );
817+ return currentHeight ;
818+ }
819+
820+ return calculateTargetHeight (jse , minHeight , currentHeight );
821+ }
822+
823+ // Reads minHeight from snapshot options first, then falls back to CLI snapshot config.
824+ private Integer resolveConfiguredMinHeight (Map <String , Object > options ) {
825+ Object minHeightObj = options .get ("minHeight" );
826+ if (minHeightObj == null && cliConfig != null && cliConfig .has ("snapshot" )) {
827+ JSONObject snapshotConfig = cliConfig .getJSONObject ("snapshot" );
828+ if (snapshotConfig .has ("minHeight" )) {
829+ minHeightObj = snapshotConfig .getInt ("minHeight" );
830+ }
831+ }
832+
833+ if (minHeightObj == null ) {
834+ return null ;
835+ }
836+
837+ try {
838+ return Integer .parseInt (minHeightObj .toString ());
839+ } catch (NumberFormatException e ) {
840+ log ("Invalid minHeight value " + minHeightObj + "; expected integer, using current window height instead." , "debug" );
841+ return null ;
842+ }
843+ }
844+
845+ // Converts content minHeight into browser outer height while preserving fallback behavior.
846+ private int calculateTargetHeight (JavascriptExecutor jse , int minHeight , int fallbackHeight ) {
847+ Object result = jse .executeScript ("return window.outerHeight - window.innerHeight + " + minHeight );
848+ if (result instanceof Number ) {
849+ return ((Number ) result ).intValue ();
850+ }
851+ return fallbackHeight ;
852+ }
853+
788854 public List <Map <String , Object >> captureResponsiveDom (WebDriver driver , Set <Cookie > cookies , Map <String , Object > options ) {
789855 List <Integer > responsiveWidths = extractResponsiveWidths (options );
790856 List <Map <String , Object >> widths = getResponsiveWidths (responsiveWidths );
@@ -796,32 +862,7 @@ public List<Map<String, Object>> captureResponsiveDom(WebDriver driver, Set<Cook
796862 int resizeCount = 0 ;
797863 JavascriptExecutor jse = (JavascriptExecutor ) driver ;
798864 jse .executeScript ("PercyDOM.waitForResize()" );
799- int targetHeight = currentHeight ;
800-
801- if (PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT ) {
802- Object minHeightObj = options .get ("minHeight" );
803- if (minHeightObj == null && cliConfig != null && cliConfig .has ("snapshot" )) {
804- JSONObject snapshotConfig = cliConfig .getJSONObject ("snapshot" );
805- if (snapshotConfig .has ("minHeight" )) {
806- minHeightObj = snapshotConfig .getInt ("minHeight" );
807- }
808- }
809- if (minHeightObj != null ) {
810- try {
811- int minHeight = Integer .parseInt (minHeightObj .toString ());
812- Object result = jse .executeScript ("return window.outerHeight - window.innerHeight + " + minHeight );
813- if (result instanceof Number ) {
814- targetHeight = ((Number ) result ).intValue ();
815- }
816- } catch (NumberFormatException e ) {
817- log ("Invalid minHeight value " + minHeightObj + "; expected integer, using current window height instead." , "debug" );
818- }
819- } else {
820- log ("minHeight not found in options or cliConfig, using current window height: " + targetHeight , "debug" );
821- }
822- } else {
823- log ("PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT is disabled, using current window height: " + targetHeight , "debug" );
824- }
865+ int targetHeight = resolveResponsiveTargetHeight (options , jse , currentHeight );
825866 for (Map <String , Object > widthMap : widths ) {
826867 Object widthObj = widthMap .get ("width" );
827868 if (!(widthObj instanceof Number )) {
0 commit comments