Skip to content

Commit d88d8dd

Browse files
fix: the base64 url gave issues in some browsers (#971)
* fix: the base64 url gave issues in some browsers - change it to blob - update tests - added BS config * fix: improve isLikelyEmulated check * chore: add release notes and fix UT's
1 parent fd6d9db commit d88d8dd

File tree

13 files changed

+488
-31
lines changed

13 files changed

+488
-31
lines changed

.changeset/dull-snakes-admire.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"webdriver-image-comparison": patch
3+
"@wdio/visual-service": patch
4+
---
5+
6+
Optimize Mobile and Emulated device support
7+
8+
## 🐛 Bugfixes
9+
10+
### #969 Fix layer injection on mobile devices
11+
12+
On some devices the layer injection, to determine the exact position of the webview, was failing. It exceeded the appium timeout and returned an error like
13+
14+
```logs
15+
[1] [0-0] 2025-05-23T08:04:11.788Z INFO webdriver: COMMAND getUrl()
16+
[1] [0-0] 2025-05-23T08:04:11.789Z INFO webdriver: [GET] https://hub-cloud.browserstack.com/wd/hub/session/xxxxx/url
17+
[1] [0-0] 2025-05-23T08:04:12.036Z INFO webdriver: RESULT about:blank
18+
[1] [0-0] 2025-05-23T08:04:12.038Z INFO webdriver: COMMAND navigateTo("data:text/html;base64,CiAgICAgICAgPG .... LONG LIST OF CHARACTERS=")
19+
[1] [0-0] 2025-05-23T08:04:12.038Z INFO webdriver: [POST] https://hub-cloud.browserstack.com/wd/hub/session/xxxx/url
20+
[1] [0-0] 2025-05-23T08:04:12.038Z INFO webdriver: DATA {
21+
[1] [0-0] url: 'data:text/html;base64,CiAgICAgICAgPGh0bWw.... LONG LIST OF CHARACTERS='
22+
[1] [0-0] }
23+
[1] [0-0] 2025-05-23T08:05:42.132Z ERROR @wdio/utils:shim: Error: WebDriverError: The operation was aborted due to timeout when running "url" with method "POST" and args "{"url":"data:text/html;base64,CiAgICAgICAgPGh0b.... LONG LIST OF CHARACTERS="}"
24+
[1] [0-0] at FetchRequest._libRequest (file:///xxxxxxx/node_modules/webdriver/build/node.js:1836:13)
25+
[1] [0-0] 2025-05-23T08:05:42.132Z DEBUG @wdio/utils:shim: Finished to run "before" hook in 91147ms
26+
[1] [0-0] at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
27+
[1] [0-0] at async FetchRequest._request (file:///C:/xxxxxx/node_modules/webdriver/build/node.js:1846:20)
28+
[1] [0-0] at Browser.wrapCommandFn (c:/Projects/xxxxxx/node_modules/@wdio/utils/build/index.js:907:23)
29+
[1] [0-0] at Browser.url (c:/Projects/xxxxxxx/node_modules/webdriverio/build/node.js:5682:3)
30+
[1] [0-0] at Browser.wrapCommandFn (c:/Projects/xxxxxx/node_modules/@wdio/utils/build/index.js:907:23)
31+
[1] [0-0] at async loadBase64Html (file:///C:/Projects/xxxxxx/node_modules/webdriver-image-comparison/dist/helpers/utils.js:377:5)
32+
[1] [0-0] at async getMobileViewPortPosition (file:///C:/Projects/xxxxxx/node_modules/webdriver-image-comparison/dist/helpers/utils.js:417:9)
33+
[1] [0-0] at async getMobileInstanceData (file:///C:/Projects/xxxxxx/node_modules/@wdio/visual-service/dist/utils.js:58:28)
34+
[1] [0-0] at async getInstanceData (file:///C:/Projects/xxxxxxx/node_modules/@wdio/visual-service/dist/utils.js:189:77)
35+
[1] [0-0] 2025-05-23T08:05:42.144Z INFO @wdio/browserstack-service: Update job with sessionId xxxxx
36+
```
37+
38+
This was caused by the `await url(`data:text/html;base64,${base64Html}`)` that injected the layer. Some browsers couldn't handle the `data:text/html;base64`.
39+
40+
We now fixed that with a different injection. It was tested on Android/iOS and on Sims/Emus/Real Devices and it worked
41+
42+
### Improve determining if a device is emulated
43+
44+
In a previous release we added a function to determine if a device was emulated. This resulted in incorrect screen sizes that were used for the files names for devices. This caused or failing baselines, or new files to be created because the screen sizes were not available
45+
We now improved the check and the correct screen sizes are added again to the file names and made sure that the previous generated base line could be used again.
46+
47+
## Committers: 1
48+
49+
- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"test.ocr.saucelabs.desktop": "SAUCE=true wdio tests/configs/wdio.ocr.saucelabs.conf.ts",
3535
"test.ocr.lambdatest.desktop": "wdio tests/configs/wdio.ocr.lambdatest.conf.ts",
3636
"test.unit.coverage": "vitest --coverage",
37+
"test.bs.real.device": "wdio tests/configs/browserstack.real.device.conf.ts",
3738
"test.saucelabs.app": "wdio ./tests/configs/wdio.saucelabs.app.conf.ts",
3839
"test.saucelabs.emu.app": "cross-env SAUCE_ENV=emu wdio ./tests/configs/wdio.saucelabs.app.conf.ts",
3940
"test.saucelabs.sims.app": "cross-env SAUCE_ENV=sims wdio ./tests/configs/wdio.saucelabs.app.conf.ts",
@@ -71,6 +72,7 @@
7172
"@vitest/coverage-v8": "^3.1.1",
7273
"@vitest/ui": "^3.1.1",
7374
"@wdio/appium-service": "^9.13.0",
75+
"@wdio/browserstack-service": "^9.14.0",
7476
"@wdio/cli": "^9.14.0",
7577
"@wdio/local-runner": "^9.14.0",
7678
"@wdio/sauce-service": "^9.14.0",
@@ -95,4 +97,4 @@
9597
"wdio-lambdatest-service": "^4.0.0"
9698
},
9799
"packageManager": "[email protected]+sha256.cf86a7ad764406395d4286a6d09d730711720acc6d93e9dce9ac7ac4dc4a28a7"
98-
}
100+
}

packages/webdriver-image-comparison/src/clientSideScripts/__snapshots__/getScreenDimensions.test.ts.snap

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ exports[`getScreenDimensions > should get the needed screen dimensions 1`] = `
2929
}
3030
`;
3131

32+
exports[`getScreenDimensions > should get the needed screen dimensions for a real device 1`] = `
33+
{
34+
"dimensions": {
35+
"body": {
36+
"offsetHeight": 0,
37+
"scrollHeight": 0,
38+
},
39+
"html": {
40+
"clientHeight": 0,
41+
"clientWidth": 0,
42+
"offsetHeight": 0,
43+
"scrollHeight": 0,
44+
"scrollWidth": 0,
45+
},
46+
"window": {
47+
"devicePixelRatio": 1,
48+
"innerHeight": 768,
49+
"innerWidth": 1024,
50+
"isEmulated": false,
51+
"isLandscape": true,
52+
"outerHeight": 768,
53+
"outerWidth": 1024,
54+
"screenHeight": 0,
55+
"screenWidth": 0,
56+
},
57+
},
58+
}
59+
`;
60+
3261
exports[`getScreenDimensions > should get the needed screen dimensions if the outerHeight and outerWidth are set to 0 1`] = `
3362
{
3463
"dimensions": {

packages/webdriver-image-comparison/src/clientSideScripts/getScreenDimensions.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@ import { CONFIGURABLE } from '../mocks/mocks.js'
55
import getScreenDimensions from './getScreenDimensions.js'
66

77
describe('getScreenDimensions', () => {
8+
it('should get the needed screen dimensions for a real device', () => {
9+
Object.defineProperty(window, 'matchMedia', {
10+
value: vi.fn().mockImplementation(() => ({
11+
matches: true,
12+
})),
13+
...CONFIGURABLE,
14+
})
15+
expect(getScreenDimensions(true)).toMatchSnapshot()
16+
})
17+
818
it('should get the needed screen dimensions', () => {
919
Object.defineProperty(window, 'matchMedia', {
1020
value: vi.fn().mockImplementation(() => ({
1121
matches: true,
1222
})),
1323
...CONFIGURABLE,
1424
})
15-
expect(getScreenDimensions()).toMatchSnapshot()
25+
expect(getScreenDimensions(false)).toMatchSnapshot()
1626
})
1727

1828
it('should get the needed screen dimensions if the outerHeight and outerWidth are set to 0', () => {
@@ -27,7 +37,7 @@ describe('getScreenDimensions', () => {
2737
...CONFIGURABLE,
2838
})
2939

30-
expect(getScreenDimensions()).toMatchSnapshot()
40+
expect(getScreenDimensions(false)).toMatchSnapshot()
3141
})
3242

3343
it('should return zeroed dimensions if the document attributes are null', () => {
@@ -40,7 +50,7 @@ describe('getScreenDimensions', () => {
4050
...CONFIGURABLE,
4151
})
4252

43-
expect(getScreenDimensions()).toMatchSnapshot()
53+
expect(getScreenDimensions(false)).toMatchSnapshot()
4454
})
4555

4656
it('should detect mobile emulation and return emulated dimensions', () => {
@@ -67,7 +77,7 @@ describe('getScreenDimensions', () => {
6777
...CONFIGURABLE,
6878
})
6979

70-
const dimensions = getScreenDimensions()
80+
const dimensions = getScreenDimensions(false)
7181

7282
Object.defineProperty(window, 'screen', {
7383
value: originalScreen,
@@ -106,7 +116,7 @@ describe('getScreenDimensions', () => {
106116
...CONFIGURABLE,
107117
})
108118

109-
const dimensions = getScreenDimensions()
119+
const dimensions = getScreenDimensions(false)
110120

111121
Object.defineProperty(window, 'screen', {
112122
value: originalScreen,
@@ -145,7 +155,7 @@ describe('getScreenDimensions', () => {
145155
...CONFIGURABLE,
146156
})
147157

148-
const dimensions = getScreenDimensions()
158+
const dimensions = getScreenDimensions(false)
149159

150160
Object.defineProperty(window, 'screen', {
151161
value: originalScreen,

packages/webdriver-image-comparison/src/clientSideScripts/getScreenDimensions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import type { ScreenDimensions } from './screenDimensions.interfaces.js'
33
/**
44
* Get all the screen dimensions
55
*/
6-
export default function getScreenDimensions(): ScreenDimensions {
6+
export default function getScreenDimensions(isMobile: boolean): ScreenDimensions {
77
// We need to determine if the screen is emulated, because that would return different values
88
const width = window.innerWidth
99
const height = window.innerHeight
1010
const dpr = window.devicePixelRatio || 1
1111
const minEdge = Math.min(width, height)
1212
const maxEdge = Math.max(width, height)
1313
const isLikelyEmulated =
14+
!isMobile && // Only check for emulated on desktop
1415
dpr >= 2 && // High-DPI signal
1516
minEdge <= 800 && // Catch phones/tablets in portrait/landscape
1617
maxEdge <= 1280 && // Conservative max for emulated tablet sizes

packages/webdriver-image-comparison/src/helpers/utils.test.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -673,27 +673,25 @@ describe('utils', () => {
673673
})
674674

675675
describe('loadBase64Html', () => {
676-
const mockUrl = vi.fn()
677676
const mockExecutor = vi.fn()
678677

679678
afterEach(() => {
680679
vi.clearAllMocks()
681680
})
682681

683-
it('should call url with base64 html and skip executor for Android', async () => {
684-
await loadBase64Html({ executor: mockExecutor, isIOS: false, url: mockUrl })
682+
it('should call executor with blob URL creation for all platforms', async () => {
683+
await loadBase64Html({ executor: mockExecutor, isIOS: false })
685684

686-
expect(mockUrl).toHaveBeenCalledTimes(1)
687-
expect(mockUrl.mock.calls[0][0]).toMatch(/^data:text\/html;base64,/)
688-
expect(mockExecutor).not.toHaveBeenCalled()
685+
expect(mockExecutor).toHaveBeenCalledTimes(1)
686+
expect(mockExecutor).toHaveBeenCalledWith(expect.any(Function), expect.any(String))
689687
})
690688

691-
it('should call url with base64 html and call executor for iOS', async () => {
692-
await loadBase64Html({ executor: mockExecutor, isIOS: true, url: mockUrl })
689+
it('should call executor with blob URL creation and checkMetaTag for iOS', async () => {
690+
await loadBase64Html({ executor: mockExecutor, isIOS: true })
693691

694-
expect(mockUrl).toHaveBeenCalledTimes(1)
695-
expect(mockUrl.mock.calls[0][0]).toMatch(/^data:text\/html;base64,/)
696-
expect(mockExecutor).toHaveBeenCalledWith(checkMetaTag)
692+
expect(mockExecutor).toHaveBeenCalledTimes(2)
693+
expect(mockExecutor).toHaveBeenNthCalledWith(1, expect.any(Function), expect.any(String))
694+
expect(mockExecutor).toHaveBeenNthCalledWith(2, checkMetaTag)
697695
})
698696
})
699697

@@ -733,7 +731,7 @@ describe('utils', () => {
733731
logWarnMock.mockRestore()
734732
})
735733

736-
it('should throw the error if its not a known Appium command error', async () => {
734+
it('should throw the error if it\'s not a known Appium command error', async () => {
737735
const executor = vi.fn().mockRejectedValueOnce(new Error('Some unexpected error'))
738736

739737
await expect(executeNativeClick({ executor, isIOS: false, ...coords }))
@@ -767,6 +765,7 @@ describe('utils', () => {
767765

768766
it('should return correct device rectangles for iOS WebView flow', async () => {
769767
mockExecutor
768+
.mockResolvedValueOnce(undefined) // executor for the blob (loadBase64Html)
770769
.mockResolvedValueOnce(undefined) // checkMetaTag (loadBase64Html)
771770
.mockResolvedValueOnce(undefined) // injectWebviewOverlay
772771
.mockResolvedValueOnce(undefined) // nativeClick
@@ -778,7 +777,7 @@ describe('utils', () => {
778777
})
779778

780779
expect(mockGetUrl).toHaveBeenCalled()
781-
expect(mockUrl).toHaveBeenCalledTimes(2)
780+
expect(mockUrl).toHaveBeenCalledTimes(1)
782781
expect(mockExecutor).toHaveBeenCalledWith(injectWebviewOverlay, false)
783782
expect(mockExecutor).toHaveBeenCalledWith(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]')
784783

packages/webdriver-image-comparison/src/helpers/utils.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ export async function getMobileScreenSize({
432432
/**
433433
* Load a base64 HTML page in the browser
434434
*/
435-
export async function loadBase64Html({ executor, isIOS, url }: {executor:Executor, isIOS:boolean, url:any}): Promise<void> {
435+
export async function loadBase64Html({ executor, isIOS }: {executor:Executor, isIOS:boolean}): Promise<void> {
436436
const htmlContent = `
437437
<html>
438438
<head>
@@ -457,9 +457,11 @@ export async function loadBase64Html({ executor, isIOS, url }: {executor:Executo
457457
</body>
458458
</html>`
459459

460-
const base64Html = Buffer.from(htmlContent).toString('base64')
461-
462-
await url(`data:text/html;base64,${base64Html}`)
460+
await executor((htmlContent) => {
461+
const blob = new Blob([htmlContent], { type: 'text/html' })
462+
const blobUrl = URL.createObjectURL(blob)
463+
window.location.href = blobUrl
464+
}, htmlContent)
463465

464466
if (isIOS) {
465467
await executor(checkMetaTag)
@@ -519,7 +521,7 @@ export async function getMobileViewPortPosition({
519521
if (!isNativeContext && (isIOS || (isAndroid && nativeWebScreenshot))) {
520522
const currentUrl = await getUrl()
521523
// 1. Load a base64 HTML page
522-
await loadBase64Html({ executor, isIOS, url })
524+
await loadBase64Html({ executor, isIOS })
523525
// 2. Inject an overlay on top of the webview with an event listener that stores the click position in the webview
524526
await executor(injectWebviewOverlay, isAndroid)
525527
// 3. Click on the overlay in the center of the screen with a native click

packages/webdriver-image-comparison/src/methods/instanceData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default async function getEnrichedInstanceData(
2222
addShadowPadding: boolean,
2323
): Promise<EnrichedInstanceData> {
2424
// Get the current browser data
25-
const browserData = await executor(getScreenDimensions)
25+
const browserData = await executor(getScreenDimensions, instanceOptions.isMobile)
2626
const { addressBarShadowPadding, toolBarShadowPadding, browserName, nativeWebScreenshot, platformName } = instanceOptions
2727

2828
// Determine some constants

0 commit comments

Comments
 (0)