diff --git a/.changeset/empty-dots-follow.md b/.changeset/empty-dots-follow.md new file mode 100644 index 00000000..33df3a99 --- /dev/null +++ b/.changeset/empty-dots-follow.md @@ -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)) diff --git a/packages/image-comparison-core/src/methods/screenshots.ts b/packages/image-comparison-core/src/methods/screenshots.ts index f97220a0..258af8ac 100644 --- a/packages/image-comparison-core/src/methods/screenshots.ts +++ b/packages/image-comparison-core/src/methods/screenshots.ts @@ -315,6 +315,18 @@ 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 @@ -322,7 +334,15 @@ export async function getDesktopFullPageScreenshotsData(browserInstance:Webdrive 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 @@ -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, diff --git a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png index 1e86f4df..969b7b07 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png index cded61b0..bc0b87c6 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png differ