Skip to content

Commit 79d2b1d

Browse files
Ws/desktop safari fullpage bezel issue (#1073)
* fix: fix Safari desktop dropshadow crop * fix: last fixes for the rounded bottom corners * chore: revert pushed test changes * chore: update safari baseline image * chore: add some comments * fix: add edge cases * chore: add changeset and revert test
1 parent 782b98a commit 79d2b1d

File tree

4 files changed

+138
-12
lines changed

4 files changed

+138
-12
lines changed

.changeset/empty-dots-follow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@wdio/image-comparison-core": patch
3+
"@wdio/visual-service": patch
4+
---
5+
6+
# 🐛 Bugfixes
7+
8+
## #1073 Normalize Safari desktop screenshots by trimming macOS window corner radius and top window shadow
9+
10+
Safari desktop screenshots included the macOS window mask at the bottom and a shadow at the top. These artifacts caused incorrect detection of the viewable area for full page screenshots, which resulted in misaligned stitching. The viewable region is now calculated correctly by trimming these areas.
11+
12+
# Committers: 1
13+
14+
- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))

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

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,14 +315,34 @@ export async function getDesktopFullPageScreenshotsData(browserInstance:Webdrive
315315
const { devicePixelRatio, fullPageScrollTimeout, hideAfterFirstScroll, innerHeight } = options
316316
let actualInnerHeight = innerHeight
317317

318+
const { capabilities } = browserInstance
319+
const browserName = (capabilities?.browserName || '').toLowerCase()
320+
// Safari desktop returns the browser mask with rounded corners and a drop shadow, so we need to fix this
321+
const isSafariDesktop = browserName.includes('safari') && !browserInstance.isMobile
322+
const safariTopDropShadowCssPixels = isSafariDesktop ? Math.round(1 * devicePixelRatio) : 0
323+
const safariBottomCropOffsetCssPixels = isSafariDesktop ? Math.round(10 * devicePixelRatio) : 0
324+
// For Safari desktop, calculate effective scroll increment
325+
// First image: scroll by 0, use full height (e.g.716px), crop 10px from bottom
326+
// Subsequent images: scroll by (actualInnerHeight - dropShadowOffset - bottomCropOffset) = 705px, crop 1px from top and 10px from bottom
327+
const effectiveScrollIncrement = isSafariDesktop
328+
? actualInnerHeight - safariTopDropShadowCssPixels - safariBottomCropOffsetCssPixels
329+
: actualInnerHeight
318330
// Start with an empty array, during the scroll it will be filled because a page could also have a lazy loading
319331
const amountOfScrollsArray = []
320332
let scrollHeight: number | undefined
321333
let screenshotSize
322334

323335
for (let i = 0; i <= amountOfScrollsArray.length; i++) {
324336
// Determine and start scrolling
325-
const scrollY = actualInnerHeight * i
337+
// For Safari desktop: first image scrolls to 0, subsequent images scroll by effectiveScrollIncrement (715px)
338+
// Image 0: scrollY = 0
339+
// Image 1: scrollY = 715 (effectiveScrollIncrement)
340+
// Image 2: scrollY = 1430 (2 * effectiveScrollIncrement)
341+
// etc.
342+
const scrollY = isSafariDesktop
343+
? (i === 0 ? 0 : i * effectiveScrollIncrement)
344+
: actualInnerHeight * i
345+
326346
await browserInstance.execute(scrollToPosition, scrollY)
327347

328348
// Simply wait the amount of time specified for lazy-loading
@@ -351,30 +371,122 @@ export async function getDesktopFullPageScreenshotsData(browserInstance:Webdrive
351371
// and SafariDriver for Safari 11
352372
}
353373

354-
// Determine scroll height and check if we need to scroll again
355374
scrollHeight = await browserInstance.execute(getDocumentScrollHeight)
356375

357-
if (scrollHeight && (scrollY + actualInnerHeight < scrollHeight) && screenshotSize.height === actualInnerHeight) {
376+
// For Safari desktop, use effectiveScrollIncrement for the scroll check
377+
const scrollCheckHeight = isSafariDesktop ? effectiveScrollIncrement : actualInnerHeight
378+
379+
if (scrollHeight && (scrollY + scrollCheckHeight < scrollHeight) && screenshotSize.height === actualInnerHeight) {
358380
amountOfScrollsArray.push(amountOfScrollsArray.length)
359381
}
360-
// There is no else, Lazy load and large screenshots,
361-
// like with older drivers such as FF <= 47 and IE11, will not work
362382

363383
// The height of the image of the last 1 could be different
364-
const imageHeight: number = scrollHeight && amountOfScrollsArray.length === i
365-
? scrollHeight - actualInnerHeight * viewportScreenshots.length
366-
: screenshotSize.height
384+
// For Safari desktop, account for first image being full height and subsequent images being cropped
385+
const isFirstImage = i === 0
386+
const isLastImage = amountOfScrollsArray.length === i
387+
let imageHeight: number
388+
if (scrollHeight && isLastImage) {
389+
if (isSafariDesktop) {
390+
// Calculate remaining content: scrollHeight - (firstImageHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement)
391+
const numberOfPreviousImages = viewportScreenshots.length
392+
const totalPreviousHeight = numberOfPreviousImages === 0
393+
? 0
394+
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
395+
const remainingContent = scrollHeight - totalPreviousHeight
396+
// For the last image, we need to be smart:
397+
// - If remainingContent >= actualInnerHeight: it's a full screenshot, treat it like a regular non-first image
398+
// (crop 1px from top, visible height = 705px, but last image doesn't crop bottom, so add 10px)
399+
// - If remainingContent < actualInnerHeight: it's a partial screenshot
400+
// For partial screenshots, we're cropping from a position that doesn't include the drop shadow at pixel 0
401+
// Last image doesn't crop bottom, so we need to add 10px to account for that
402+
imageHeight = remainingContent >= actualInnerHeight
403+
? effectiveScrollIncrement + safariBottomCropOffsetCssPixels
404+
: remainingContent + safariBottomCropOffsetCssPixels
405+
} else {
406+
imageHeight = scrollHeight - actualInnerHeight * viewportScreenshots.length
407+
}
408+
} else {
409+
// Non-last images: use full height for first, effectiveScrollIncrement for subsequent
410+
// For non-first images, effectiveScrollIncrement already accounts for top and bottom crops
411+
imageHeight = isSafariDesktop && !isFirstImage
412+
? effectiveScrollIncrement
413+
: screenshotSize.height
414+
}
415+
367416
// The starting position for cropping could be different for the last image (0 means no cropping)
368-
const imageYPosition = amountOfScrollsArray.length === i && amountOfScrollsArray.length !== 0
369-
? actualInnerHeight - imageHeight
370-
: 0
417+
// For Safari desktop, crop 1px from top for all images except first
418+
if (isSafariDesktop && isFirstImage && safariBottomCropOffsetCssPixels > 0) {
419+
imageHeight -= safariBottomCropOffsetCssPixels
420+
}
421+
422+
// The starting position for cropping could be different for the last image (0 means no cropping)
423+
// For Safari desktop, crop 1px from top for all images except first
424+
let imageYPosition: number
425+
if (isSafariDesktop) {
426+
if (isLastImage && !isFirstImage) {
427+
// Last image: need to handle two cases
428+
const numberOfPreviousImages = viewportScreenshots.length
429+
const totalPreviousHeight = numberOfPreviousImages === 0
430+
? 0
431+
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
432+
const remainingContent = scrollHeight ? scrollHeight - totalPreviousHeight : 0
433+
434+
// Full screenshot: treat like regular non-first image (crop 1px from top)
435+
// Partial screenshot: we want to show the last remainingContent pixels
436+
// But we need to include the bottom 10px that we're not cropping, so start 10px higher
437+
// imageHeight = remainingContent, so we start at: 716 - remainingContent - 10px
438+
// This way we crop 10px higher to include the bottom corners
439+
imageYPosition = remainingContent >= actualInnerHeight
440+
? safariTopDropShadowCssPixels
441+
: actualInnerHeight - remainingContent - safariBottomCropOffsetCssPixels
442+
443+
// If remainingContent is too small, we might get negative imageYPosition or invalid dimensions
444+
if (imageYPosition < 0) {
445+
imageYPosition = actualInnerHeight - remainingContent
446+
imageHeight = remainingContent
447+
} else if (imageYPosition + imageHeight > screenshotSize.height) {
448+
imageHeight = screenshotSize.height - imageYPosition
449+
}
450+
} else if (!isFirstImage) {
451+
// Non-last, non-first images: crop 1px from top
452+
imageYPosition = safariTopDropShadowCssPixels
453+
} else {
454+
// First image: no crop
455+
imageYPosition = 0
456+
}
457+
} else {
458+
imageYPosition = isLastImage && !isFirstImage
459+
? actualInnerHeight - imageHeight
460+
: 0
461+
}
462+
463+
// Ensure imageYPosition and imageHeight are valid for all cases
464+
if (imageYPosition < 0) {
465+
imageHeight += imageYPosition
466+
imageYPosition = 0
467+
}
468+
if (imageYPosition + imageHeight > screenshotSize.height) {
469+
imageHeight = screenshotSize.height - imageYPosition
470+
}
471+
472+
// Calculate based on where the previous image ends
473+
// Previous image's canvasYPosition + previous image's height
474+
let canvasYPosition: number
475+
if (isSafariDesktop && !isFirstImage) {
476+
const previousImage = viewportScreenshots[viewportScreenshots.length - 1]
477+
canvasYPosition = previousImage
478+
? previousImage.canvasYPosition + previousImage.imageHeight
479+
: actualInnerHeight + (i - 1) * effectiveScrollIncrement
480+
} else {
481+
canvasYPosition = isSafariDesktop ? 0 : scrollY
482+
}
371483

372484
// Store all the screenshot data in the screenshot object
373485
viewportScreenshots.push({
374486
...calculateDprData(
375487
{
376488
canvasWidth: screenshotSize.width,
377-
canvasYPosition: scrollY,
489+
canvasYPosition: canvasYPosition,
378490
imageHeight: imageHeight,
379491
imageWidth: screenshotSize.width,
380492
imageXPosition: 0,
199 Bytes
Loading
176 Bytes
Loading

0 commit comments

Comments
 (0)