Skip to content

Commit 86572b0

Browse files
committed
fix: fix Safari desktop dropshadow crop
1 parent 5788bb5 commit 86572b0

File tree

1 file changed

+177
-9
lines changed

1 file changed

+177
-9
lines changed

packages/image-comparison-core/src/methods/screenshots.ts

Lines changed: 177 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logger from '@wdio/logger'
2+
import { join } from 'node:path'
3+
import { promises as fsPromises } from 'node:fs'
24
import scrollToPosition from '../clientSideScripts/scrollToPosition.js'
35
import getDocumentScrollHeight from '../clientSideScripts/getDocumentScrollHeight.js'
46
import { calculateDprData, getBase64ScreenshotSize, waitFor } from '../helpers/utils.js'
@@ -13,6 +15,7 @@ import hideRemoveElements from '../clientSideScripts/hideRemoveElements.js'
1315
import hideScrollBars from '../clientSideScripts/hideScrollbars.js'
1416
import type { ElementRectanglesOptions, RectanglesOutput } from './rectangles.interfaces.js'
1517
import { determineElementRectangles } from './rectangles.js'
18+
import { cropAndConvertToDataURL, saveBase64Image } from './images.js'
1619

1720
const log = logger('@wdio/visual-service:@wdio/image-comparison-core:screenshots')
1821

@@ -277,14 +280,42 @@ export async function getDesktopFullPageScreenshotsData(browserInstance:Webdrive
277280
const { devicePixelRatio, fullPageScrollTimeout, hideAfterFirstScroll, innerHeight } = options
278281
let actualInnerHeight = innerHeight
279282

283+
// Detect Safari desktop for debug saving and drop shadow cropping
284+
const { capabilities } = browserInstance
285+
const browserName = (capabilities?.browserName || '').toLowerCase()
286+
const isSafariDesktop = browserName.includes('safari') && !browserInstance.isMobile
287+
const debugDir = isSafariDesktop ? join(process.cwd(), '.tmp', 'debug', 'safari-desktop-fullpage-screenshots') : null
288+
289+
// Safari desktop has a drop shadow at the top
290+
// The drop shadow is always 1 device pixel, which translates to 1 * DPR CSS pixels
291+
const safariTopDropShadowCssPixels = isSafariDesktop ? Math.round(1 * devicePixelRatio) : 0
292+
// For Safari desktop, calculate effective scroll increment
293+
// First image: scroll by 0, use full height (716px)
294+
// Subsequent images: scroll by (actualInnerHeight - dropShadowOffset) = 715px, crop 1px from top
295+
const effectiveScrollIncrement = isSafariDesktop
296+
? actualInnerHeight - safariTopDropShadowCssPixels
297+
: actualInnerHeight
298+
299+
// Create debug directory if needed
300+
if (debugDir) {
301+
await fsPromises.mkdir(debugDir, { recursive: true })
302+
}
303+
280304
// Start with an empty array, during the scroll it will be filled because a page could also have a lazy loading
281305
const amountOfScrollsArray = []
282306
let scrollHeight: number | undefined
283307
let screenshotSize
284308

285309
for (let i = 0; i <= amountOfScrollsArray.length; i++) {
286310
// Determine and start scrolling
287-
const scrollY = actualInnerHeight * i
311+
// For Safari desktop: first image scrolls to 0, subsequent images scroll by effectiveScrollIncrement (715px)
312+
// Image 0: scrollY = 0
313+
// Image 1: scrollY = 715 (effectiveScrollIncrement)
314+
// Image 2: scrollY = 1430 (2 * effectiveScrollIncrement)
315+
// etc.
316+
const scrollY = isSafariDesktop
317+
? (i === 0 ? 0 : i * effectiveScrollIncrement)
318+
: actualInnerHeight * i
288319
await browserInstance.execute(scrollToPosition, scrollY)
289320

290321
// Simply wait the amount of time specified for lazy-loading
@@ -316,27 +347,164 @@ export async function getDesktopFullPageScreenshotsData(browserInstance:Webdrive
316347
// Determine scroll height and check if we need to scroll again
317348
scrollHeight = await browserInstance.execute(getDocumentScrollHeight)
318349

319-
if (scrollHeight && (scrollY + actualInnerHeight < scrollHeight) && screenshotSize.height === actualInnerHeight) {
350+
// For Safari desktop, use effectiveScrollIncrement for the scroll check
351+
const scrollCheckHeight = isSafariDesktop ? effectiveScrollIncrement : actualInnerHeight
352+
if (scrollHeight && (scrollY + scrollCheckHeight < scrollHeight) && screenshotSize.height === actualInnerHeight) {
320353
amountOfScrollsArray.push(amountOfScrollsArray.length)
321354
}
322355
// There is no else, Lazy load and large screenshots,
323356
// like with older drivers such as FF <= 47 and IE11, will not work
324357

325358
// The height of the image of the last 1 could be different
326-
const imageHeight: number = scrollHeight && amountOfScrollsArray.length === i
327-
? scrollHeight - actualInnerHeight * viewportScreenshots.length
328-
: screenshotSize.height
359+
// For Safari desktop, account for first image being full height and subsequent images being cropped
360+
const isFirstImage = i === 0
361+
const isLastImage = amountOfScrollsArray.length === i
362+
let imageHeight: number
363+
if (scrollHeight && isLastImage) {
364+
if (isSafariDesktop) {
365+
// Calculate remaining content: scrollHeight - (firstImageHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement)
366+
const numberOfPreviousImages = viewportScreenshots.length
367+
const totalPreviousHeight = numberOfPreviousImages === 0
368+
? 0
369+
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
370+
const remainingContent = scrollHeight - totalPreviousHeight
371+
372+
// For the last image, we need to be smart:
373+
// - If remainingContent >= actualInnerHeight: it's a full screenshot, treat it like a regular non-first image
374+
// (crop 1px from top, visible height = 715px)
375+
// - If remainingContent < actualInnerHeight: it's a partial screenshot
376+
// For partial screenshots, we're cropping from a position that doesn't include the drop shadow at pixel 0
377+
// So we don't need to add 1px - just use remainingContent directly
378+
imageHeight = remainingContent >= actualInnerHeight
379+
? effectiveScrollIncrement // Full screenshot: treat like regular non-first image
380+
: remainingContent // Partial screenshot: use remainingContent directly (no drop shadow in cropped region)
381+
} else {
382+
imageHeight = scrollHeight - actualInnerHeight * viewportScreenshots.length
383+
}
384+
} else {
385+
// Non-last images: use full height for first, effectiveScrollIncrement for subsequent
386+
imageHeight = isSafariDesktop && !isFirstImage
387+
? effectiveScrollIncrement
388+
: screenshotSize.height
389+
}
390+
329391
// The starting position for cropping could be different for the last image (0 means no cropping)
330-
const imageYPosition = amountOfScrollsArray.length === i && amountOfScrollsArray.length !== 0
331-
? actualInnerHeight - imageHeight
332-
: 0
392+
// For Safari desktop, crop 1px from top for all images except first
393+
let imageYPosition: number
394+
if (isSafariDesktop) {
395+
if (isLastImage && !isFirstImage) {
396+
// Last image: need to handle two cases
397+
const numberOfPreviousImages = viewportScreenshots.length
398+
const totalPreviousHeight = numberOfPreviousImages === 0
399+
? 0
400+
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
401+
const remainingContent = scrollHeight ? scrollHeight - totalPreviousHeight : 0
402+
403+
// Full screenshot: treat like regular non-first image (crop 1px from top)
404+
// Partial screenshot: we want to show the last remainingContent pixels
405+
// imageHeight = remainingContent, so we start at: 716 - remainingContent
406+
// For partial screenshots, the drop shadow is at pixel 0, but we're cropping from a different position
407+
// so we don't need to crop the drop shadow - just position to get the last remainingContent pixels
408+
imageYPosition = remainingContent >= actualInnerHeight
409+
? safariTopDropShadowCssPixels
410+
: actualInnerHeight - remainingContent
411+
} else if (!isFirstImage) {
412+
// Non-last, non-first images: crop 1px from top
413+
imageYPosition = safariTopDropShadowCssPixels
414+
} else {
415+
// First image: no crop
416+
imageYPosition = 0
417+
}
418+
} else {
419+
// Non-Safari: standard calculation
420+
imageYPosition = isLastImage && !isFirstImage
421+
? actualInnerHeight - imageHeight
422+
: 0
423+
}
424+
425+
// Debug logging for Safari desktop to see what's being cropped
426+
if (isSafariDesktop) {
427+
// For non-first images, imageHeight already accounts for the crop, so visible = imageHeight
428+
// For first image, visible = imageHeight (no crop)
429+
const visibleHeightAfterCrop = imageHeight
430+
console.log(`Safari desktop screenshot ${i} cropping details:`, {
431+
isFirstImage,
432+
isLastImage,
433+
scrollY,
434+
screenshotSize: {
435+
width: screenshotSize.width,
436+
height: screenshotSize.height,
437+
},
438+
imageHeight,
439+
imageYPosition,
440+
visibleHeightAfterCrop,
441+
actualInnerHeight,
442+
effectiveScrollIncrement,
443+
remainingContent: isLastImage && scrollHeight
444+
? (() => {
445+
const numberOfPreviousImages = viewportScreenshots.length
446+
const totalPreviousHeight = numberOfPreviousImages === 0
447+
? 0
448+
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
449+
return scrollHeight - totalPreviousHeight
450+
})()
451+
: undefined,
452+
cropFromOriginal: {
453+
startY: imageYPosition,
454+
height: imageHeight,
455+
endY: imageYPosition + imageHeight,
456+
},
457+
})
458+
}
459+
460+
// Calculate canvasYPosition for stitching
461+
// For Safari desktop: first image at 0, subsequent images start where previous image ends
462+
let canvasYPosition: number
463+
if (isSafariDesktop && !isFirstImage) {
464+
// Calculate based on where the previous image ends
465+
// Previous image's canvasYPosition + previous image's height
466+
const previousImage = viewportScreenshots[viewportScreenshots.length - 1]
467+
canvasYPosition = previousImage
468+
? previousImage.canvasYPosition + previousImage.imageHeight
469+
: actualInnerHeight + (i - 1) * effectiveScrollIncrement // Fallback (shouldn't happen)
470+
} else {
471+
canvasYPosition = isSafariDesktop ? 0 : scrollY
472+
}
473+
474+
// Calculate crop dimensions in device pixels for debug saving
475+
const imageXPositionDevicePixels = 0
476+
const imageYPositionDevicePixels = Math.round(imageYPosition * devicePixelRatio)
477+
const imageWidthDevicePixels = Math.round(screenshotSize.width * devicePixelRatio)
478+
const imageHeightDevicePixels = Math.round(imageHeight * devicePixelRatio)
479+
480+
// Save cropped screenshot to debug folder for Safari desktop
481+
if (isSafariDesktop && debugDir) {
482+
try {
483+
const croppedBase64 = await cropAndConvertToDataURL({
484+
addIOSBezelCorners: false,
485+
base64Image: screenshot,
486+
deviceName: '',
487+
devicePixelRatio: 1, // Already in device pixels
488+
height: imageHeightDevicePixels,
489+
isIOS: false,
490+
isLandscape: false,
491+
sourceX: imageXPositionDevicePixels,
492+
sourceY: imageYPositionDevicePixels,
493+
width: imageWidthDevicePixels,
494+
})
495+
const debugFilePath = join(debugDir, `step-${i}-scrollY-${scrollY}-height-${imageHeight}-yPos-${imageYPosition}.png`)
496+
await saveBase64Image(croppedBase64, debugFilePath)
497+
} catch (error) {
498+
log.warn(`Failed to save debug screenshot for step ${i}:`, error)
499+
}
500+
}
333501

334502
// Store all the screenshot data in the screenshot object
335503
viewportScreenshots.push({
336504
...calculateDprData(
337505
{
338506
canvasWidth: screenshotSize.width,
339-
canvasYPosition: scrollY,
507+
canvasYPosition: canvasYPosition,
340508
imageHeight: imageHeight,
341509
imageWidth: screenshotSize.width,
342510
imageXPosition: 0,

0 commit comments

Comments
 (0)