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
14 changes: 14 additions & 0 deletions .changeset/empty-dots-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@wdio/image-comparison-core": patch
"@wdio/visual-service": patch
---

# 🐛 Bugfixes

## #1073 Normalize Safari desktop screenshots by trimming macOS window corner radius and top window shadow

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.

# Committers: 1

- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))
136 changes: 124 additions & 12 deletions packages/image-comparison-core/src/methods/screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,34 @@ export async function getDesktopFullPageScreenshotsData(browserInstance:Webdrive
const { devicePixelRatio, fullPageScrollTimeout, hideAfterFirstScroll, innerHeight } = options
let actualInnerHeight = innerHeight

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

for (let i = 0; i <= amountOfScrollsArray.length; i++) {
// Determine and start scrolling
const scrollY = actualInnerHeight * i
// For Safari desktop: first image scrolls to 0, subsequent images scroll by effectiveScrollIncrement (715px)
// Image 0: scrollY = 0
// Image 1: scrollY = 715 (effectiveScrollIncrement)
// Image 2: scrollY = 1430 (2 * effectiveScrollIncrement)
// etc.
const scrollY = isSafariDesktop
? (i === 0 ? 0 : i * effectiveScrollIncrement)
: actualInnerHeight * i

await browserInstance.execute(scrollToPosition, scrollY)

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

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

if (scrollHeight && (scrollY + actualInnerHeight < scrollHeight) && screenshotSize.height === actualInnerHeight) {
// For Safari desktop, use effectiveScrollIncrement for the scroll check
const scrollCheckHeight = isSafariDesktop ? effectiveScrollIncrement : actualInnerHeight

if (scrollHeight && (scrollY + scrollCheckHeight < scrollHeight) && screenshotSize.height === actualInnerHeight) {
amountOfScrollsArray.push(amountOfScrollsArray.length)
}
// There is no else, Lazy load and large screenshots,
// like with older drivers such as FF <= 47 and IE11, will not work

// The height of the image of the last 1 could be different
const imageHeight: number = scrollHeight && amountOfScrollsArray.length === i
? scrollHeight - actualInnerHeight * viewportScreenshots.length
: screenshotSize.height
// For Safari desktop, account for first image being full height and subsequent images being cropped
const isFirstImage = i === 0
const isLastImage = amountOfScrollsArray.length === i
let imageHeight: number
if (scrollHeight && isLastImage) {
if (isSafariDesktop) {
// Calculate remaining content: scrollHeight - (firstImageHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement)
const numberOfPreviousImages = viewportScreenshots.length
const totalPreviousHeight = numberOfPreviousImages === 0
? 0
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
const remainingContent = scrollHeight - totalPreviousHeight
// For the last image, we need to be smart:
// - If remainingContent >= actualInnerHeight: it's a full screenshot, treat it like a regular non-first image
// (crop 1px from top, visible height = 705px, but last image doesn't crop bottom, so add 10px)
// - If remainingContent < actualInnerHeight: it's a partial screenshot
// For partial screenshots, we're cropping from a position that doesn't include the drop shadow at pixel 0
// Last image doesn't crop bottom, so we need to add 10px to account for that
imageHeight = remainingContent >= actualInnerHeight
? effectiveScrollIncrement + safariBottomCropOffsetCssPixels
: remainingContent + safariBottomCropOffsetCssPixels
} else {
imageHeight = scrollHeight - actualInnerHeight * viewportScreenshots.length
}
} else {
// Non-last images: use full height for first, effectiveScrollIncrement for subsequent
// For non-first images, effectiveScrollIncrement already accounts for top and bottom crops
imageHeight = isSafariDesktop && !isFirstImage
? effectiveScrollIncrement
: screenshotSize.height
}

// The starting position for cropping could be different for the last image (0 means no cropping)
const imageYPosition = amountOfScrollsArray.length === i && amountOfScrollsArray.length !== 0
? actualInnerHeight - imageHeight
: 0
// For Safari desktop, crop 1px from top for all images except first
if (isSafariDesktop && isFirstImage && safariBottomCropOffsetCssPixels > 0) {
imageHeight -= safariBottomCropOffsetCssPixels
}

// The starting position for cropping could be different for the last image (0 means no cropping)
// For Safari desktop, crop 1px from top for all images except first
let imageYPosition: number
if (isSafariDesktop) {
if (isLastImage && !isFirstImage) {
// Last image: need to handle two cases
const numberOfPreviousImages = viewportScreenshots.length
const totalPreviousHeight = numberOfPreviousImages === 0
? 0
: actualInnerHeight + (numberOfPreviousImages - 1) * effectiveScrollIncrement
const remainingContent = scrollHeight ? scrollHeight - totalPreviousHeight : 0

// Full screenshot: treat like regular non-first image (crop 1px from top)
// Partial screenshot: we want to show the last remainingContent pixels
// But we need to include the bottom 10px that we're not cropping, so start 10px higher
// imageHeight = remainingContent, so we start at: 716 - remainingContent - 10px
// This way we crop 10px higher to include the bottom corners
imageYPosition = remainingContent >= actualInnerHeight
? safariTopDropShadowCssPixels
: actualInnerHeight - remainingContent - safariBottomCropOffsetCssPixels

// If remainingContent is too small, we might get negative imageYPosition or invalid dimensions
if (imageYPosition < 0) {
imageYPosition = actualInnerHeight - remainingContent
imageHeight = remainingContent
} else if (imageYPosition + imageHeight > screenshotSize.height) {
imageHeight = screenshotSize.height - imageYPosition
}
} else if (!isFirstImage) {
// Non-last, non-first images: crop 1px from top
imageYPosition = safariTopDropShadowCssPixels
} else {
// First image: no crop
imageYPosition = 0
}
} else {
imageYPosition = isLastImage && !isFirstImage
? actualInnerHeight - imageHeight
: 0
}

// Ensure imageYPosition and imageHeight are valid for all cases
if (imageYPosition < 0) {
imageHeight += imageYPosition
imageYPosition = 0
}
if (imageYPosition + imageHeight > screenshotSize.height) {
imageHeight = screenshotSize.height - imageYPosition
}

// Calculate based on where the previous image ends
// Previous image's canvasYPosition + previous image's height
let canvasYPosition: number
if (isSafariDesktop && !isFirstImage) {
const previousImage = viewportScreenshots[viewportScreenshots.length - 1]
canvasYPosition = previousImage
? previousImage.canvasYPosition + previousImage.imageHeight
: actualInnerHeight + (i - 1) * effectiveScrollIncrement
} else {
canvasYPosition = isSafariDesktop ? 0 : scrollY
}

// Store all the screenshot data in the screenshot object
viewportScreenshots.push({
...calculateDprData(
{
canvasWidth: screenshotSize.width,
canvasYPosition: scrollY,
canvasYPosition: canvasYPosition,
imageHeight: imageHeight,
imageWidth: screenshotSize.width,
imageXPosition: 0,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.