@wdio/image-comparison-core 1.1.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/base.interfaces.d.ts +7 -0
  3. package/dist/base.interfaces.d.ts.map +1 -1
  4. package/dist/clientSideScripts/injectWebviewOverlay.test.js +29 -0
  5. package/dist/clientSideScripts/scrollElementIntoView.d.ts.map +1 -1
  6. package/dist/clientSideScripts/scrollElementIntoView.js +4 -1
  7. package/dist/clientSideScripts/scrollElementIntoView.test.js +4 -2
  8. package/dist/commands/check.interfaces.d.ts +1 -1
  9. package/dist/commands/check.interfaces.d.ts.map +1 -1
  10. package/dist/commands/checkFullPageScreen.d.ts.map +1 -1
  11. package/dist/commands/checkFullPageScreen.js +7 -2
  12. package/dist/commands/checkWebElement.d.ts.map +1 -1
  13. package/dist/commands/checkWebElement.js +7 -2
  14. package/dist/commands/checkWebScreen.d.ts.map +1 -1
  15. package/dist/commands/checkWebScreen.js +10 -3
  16. package/dist/commands/checkWebScreen.test.js +43 -0
  17. package/dist/commands/fullPage.interfaces.d.ts +6 -0
  18. package/dist/commands/fullPage.interfaces.d.ts.map +1 -1
  19. package/dist/commands/save.interfaces.d.ts +3 -1
  20. package/dist/commands/save.interfaces.d.ts.map +1 -1
  21. package/dist/commands/saveElement.d.ts +1 -1
  22. package/dist/commands/saveElement.d.ts.map +1 -1
  23. package/dist/commands/saveElement.js +2 -2
  24. package/dist/commands/saveFullPageScreen.d.ts.map +1 -1
  25. package/dist/commands/saveFullPageScreen.js +23 -3
  26. package/dist/commands/saveWebElement.d.ts +1 -1
  27. package/dist/commands/saveWebElement.d.ts.map +1 -1
  28. package/dist/commands/saveWebElement.js +24 -2
  29. package/dist/commands/saveWebScreen.d.ts +1 -1
  30. package/dist/commands/saveWebScreen.d.ts.map +1 -1
  31. package/dist/commands/saveWebScreen.js +23 -3
  32. package/dist/helpers/afterScreenshot.interfaces.d.ts +2 -0
  33. package/dist/helpers/afterScreenshot.interfaces.d.ts.map +1 -1
  34. package/dist/helpers/options.d.ts.map +1 -1
  35. package/dist/helpers/options.interfaces.d.ts +10 -0
  36. package/dist/helpers/options.interfaces.d.ts.map +1 -1
  37. package/dist/helpers/options.js +1 -0
  38. package/dist/helpers/utils.d.ts +6 -0
  39. package/dist/helpers/utils.d.ts.map +1 -1
  40. package/dist/helpers/utils.interfaces.d.ts +3 -0
  41. package/dist/helpers/utils.interfaces.d.ts.map +1 -1
  42. package/dist/helpers/utils.js +95 -29
  43. package/dist/helpers/utils.test.js +121 -1
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/methods/images.d.ts.map +1 -1
  47. package/dist/methods/images.interfaces.d.ts +4 -0
  48. package/dist/methods/images.interfaces.d.ts.map +1 -1
  49. package/dist/methods/images.js +4 -2
  50. package/dist/methods/rectangles.d.ts +33 -2
  51. package/dist/methods/rectangles.d.ts.map +1 -1
  52. package/dist/methods/rectangles.interfaces.d.ts +58 -0
  53. package/dist/methods/rectangles.interfaces.d.ts.map +1 -1
  54. package/dist/methods/rectangles.js +289 -15
  55. package/dist/methods/rectangles.test.js +558 -2
  56. package/dist/methods/screenshots.interfaces.d.ts +2 -1
  57. package/dist/methods/screenshots.interfaces.d.ts.map +1 -1
  58. package/dist/methods/takeElementScreenshots.js +30 -19
  59. package/dist/methods/takeElementScreenshots.test.js +49 -22
  60. package/package.json +3 -3
@@ -12,25 +12,32 @@ export async function takeElementScreenshot(browserInstance, options, shouldUseB
12
12
  return await takeWebDriverElementScreenshot(browserInstance, options);
13
13
  }
14
14
  async function takeBiDiElementScreenshot(browserInstance, options) {
15
- let base64Image;
16
15
  const isWebDriverElementScreenshot = false;
17
- // We also need to clip the image to the element size, taking into account the DPR
18
- // and also clip it from the document, not the viewport
19
- const rect = await browserInstance.getElementRect((await options.element).elementId);
20
- const clip = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height) };
21
- const takeBiDiElementScreenshot = (origin) => takeBase64BiDiScreenshot({ browserInstance, origin, clip });
22
- try {
23
- // By default we take the screenshot from the viewport
24
- base64Image = await takeBiDiElementScreenshot('viewport');
16
+ // Fix #1129: scrollElementIntoView receives a promise
17
+ // The element might be a promise, so we need to resolve it before using it as a browser.execute() argument
18
+ // if we need to use it in browser.execute()
19
+ const element = await options.element;
20
+ // Scroll the element into the viewport so any lazy‑load / intersection
21
+ // observers are triggered. We always capture from the *document* origin,
22
+ // so the clip coordinates are document‑relative and independent of scroll.
23
+ let currentPosition;
24
+ if (options.autoElementScroll) {
25
+ currentPosition = await browserInstance.execute(scrollElementIntoView, element, options.addressBarShadowPadding);
26
+ await waitFor(100);
25
27
  }
26
- catch (err) {
27
- // But when we get a zero dimension error (meaning the element might be bigger than the
28
- // viewport or it might not be in the viewport), we need to take the screenshot from the document.
29
- const isZeroDimensionError = typeof err?.message === 'string' && err.message.includes('WebDriver Bidi command "browsingContext.captureScreenshot" failed with error: unable to capture screen - Unable to capture screenshot with zero dimensions');
30
- if (!isZeroDimensionError) {
31
- throw err;
32
- }
33
- base64Image = await takeBiDiElementScreenshot('document');
28
+ // Get the element rect and clip the screenshot. WebDriver getElementRect
29
+ // returns coordinates relative to the document origin, which matches the
30
+ // BiDi `origin: 'document'` coordinate system.
31
+ const rect = await browserInstance.getElementRect(element.elementId);
32
+ const clip = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height) };
33
+ const base64Image = await takeBase64BiDiScreenshot({
34
+ browserInstance,
35
+ origin: 'document',
36
+ clip,
37
+ });
38
+ // Restore scroll position
39
+ if (options.autoElementScroll && currentPosition) {
40
+ await browserInstance.execute(scrollToPosition, currentPosition);
34
41
  }
35
42
  return {
36
43
  base64Image,
@@ -40,10 +47,14 @@ async function takeBiDiElementScreenshot(browserInstance, options) {
40
47
  async function takeWebDriverElementScreenshot(browserInstance, options) {
41
48
  let base64Image;
42
49
  let isWebDriverElementScreenshot = false;
50
+ // Fix #1129: scrollElementIntoView receives a promise
51
+ // The element might be a promise, so we need to resolve it before using it as a browser.execute() argument
52
+ // if we need to use it in browser.execute()
53
+ const element = await options.element;
43
54
  // Scroll the element into top of the viewport and return the current scroll position
44
55
  let currentPosition;
45
56
  if (options.autoElementScroll) {
46
- currentPosition = await browserInstance.execute(scrollElementIntoView, options.element, options.addressBarShadowPadding);
57
+ currentPosition = await browserInstance.execute(scrollElementIntoView, element, options.addressBarShadowPadding);
47
58
  // We need to wait for the scroll to finish before taking the screenshot
48
59
  await waitFor(100);
49
60
  }
@@ -53,7 +64,7 @@ async function takeWebDriverElementScreenshot(browserInstance, options) {
53
64
  browserInstance,
54
65
  devicePixelRatio: options.devicePixelRatio,
55
66
  deviceRectangles: options.deviceRectangles,
56
- element: options.element,
67
+ element,
57
68
  initialDevicePixelRatio: options.initialDevicePixelRatio,
58
69
  isEmulated: options.isEmulated,
59
70
  innerHeight: options.innerHeight,
@@ -74,7 +74,7 @@ describe('takeElementScreenshot', () => {
74
74
  vi.clearAllMocks();
75
75
  });
76
76
  describe('BiDi screenshots', () => {
77
- it('should take BiDi screenshot from viewport when shouldUseBidi is true', async () => {
77
+ it('should take BiDi screenshot from document when shouldUseBidi is true', async () => {
78
78
  const result = await takeElementScreenshot(browserInstance, baseOptions, true);
79
79
  expect(result).toEqual({
80
80
  base64Image: 'bidi-screenshot-data',
@@ -83,37 +83,50 @@ describe('takeElementScreenshot', () => {
83
83
  expect(getElementRectMock).toHaveBeenCalledWith('test-element');
84
84
  expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledWith({
85
85
  browserInstance,
86
- origin: 'viewport',
86
+ origin: 'document',
87
87
  clip: { x: 10, y: 20, width: 100, height: 200 }
88
88
  });
89
89
  expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled();
90
90
  expect(makeCroppedBase64ImageSpy).not.toHaveBeenCalled();
91
91
  });
92
- it('should fallback to document screenshot when viewport fails with zero dimensions error', async () => {
93
- takeBase64BiDiScreenshotSpy.mockRejectedValueOnce(new Error('WebDriver Bidi command "browsingContext.captureScreenshot" failed with error: unable to capture screen - Unable to capture screenshot with zero dimensions'));
94
- const result = await takeElementScreenshot(browserInstance, baseOptions, true);
92
+ //
93
+ // We intentionally rely on BiDi with origin: 'document' only. If that
94
+ // ever fails, we surface the underlying error instead of silently
95
+ // falling back to a different origin with mismatched coordinates.
96
+ it('should scroll element into view when autoElementScroll is enabled', async () => {
97
+ const optionsWithScroll = { ...baseOptions, autoElementScroll: true };
98
+ executeMock.mockResolvedValueOnce(100); // previous scroll position
99
+ const result = await takeElementScreenshot(browserInstance, optionsWithScroll, true);
95
100
  expect(result).toEqual({
96
101
  base64Image: 'bidi-screenshot-data',
97
102
  isWebDriverElementScreenshot: false
98
103
  });
99
- expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(2);
100
- expect(takeBase64BiDiScreenshotSpy.mock.calls[0][0]).toEqual({
101
- browserInstance,
102
- origin: 'viewport',
103
- clip: { x: 10, y: 20, width: 100, height: 200 }
104
- });
105
- expect(takeBase64BiDiScreenshotSpy.mock.calls[1][0]).toEqual({
106
- browserInstance,
107
- origin: 'document',
108
- clip: { x: 10, y: 20, width: 100, height: 200 }
109
- });
104
+ // First call: scrollElementIntoView, second call: scrollToPosition (restore)
105
+ expect(executeMock).toHaveBeenCalledTimes(2);
106
+ expect(executeMock.mock.calls[0]).toMatchSnapshot();
107
+ expect(executeMock.mock.calls[1]).toMatchSnapshot();
108
+ expect(waitForSpy).toHaveBeenCalledWith(100);
110
109
  });
111
- it('should throw error when BiDi screenshot fails with non-zero dimension error', async () => {
112
- const error = new Error('Some other BiDi error');
113
- takeBase64BiDiScreenshotSpy.mockRejectedValueOnce(error);
114
- await expect(takeElementScreenshot(browserInstance, baseOptions, true)).rejects.toThrow(error);
115
- expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(1);
116
- expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled();
110
+ it('should not restore scroll when autoElementScroll is enabled but no previous position', async () => {
111
+ const optionsWithScroll = { ...baseOptions, autoElementScroll: true };
112
+ executeMock.mockResolvedValueOnce(undefined); // no previous position
113
+ await takeElementScreenshot(browserInstance, optionsWithScroll, true);
114
+ // Only the scrollElementIntoView call, no restore
115
+ expect(executeMock).toHaveBeenCalledTimes(1);
116
+ expect(waitForSpy).toHaveBeenCalledWith(100);
117
+ });
118
+ it('should resolve a Promise-wrapped element (ChainablePromiseElement) before passing to browser.execute()', async () => {
119
+ const resolvedElement = { elementId: 'promise-element' };
120
+ const optionsWithPromiseElement = {
121
+ ...baseOptions,
122
+ autoElementScroll: true,
123
+ element: Promise.resolve(resolvedElement),
124
+ };
125
+ executeMock.mockResolvedValueOnce(100);
126
+ await takeElementScreenshot(browserInstance, optionsWithPromiseElement, true);
127
+ expect(getElementRectMock).toHaveBeenCalledWith('promise-element');
128
+ // The resolved element (not the Promise) must be passed to browser.execute()
129
+ expect(executeMock.mock.calls[0][1]).toEqual(resolvedElement);
117
130
  });
118
131
  });
119
132
  describe('Legacy screenshots', () => {
@@ -184,6 +197,20 @@ describe('takeElementScreenshot', () => {
184
197
  expect(executeMock).toHaveBeenCalledTimes(1); // Only the scroll into view call
185
198
  expect(waitForSpy).toHaveBeenCalledWith(100);
186
199
  });
200
+ it('should resolve a Promise-wrapped element (ChainablePromiseElement) before passing to browser.execute()', async () => {
201
+ const resolvedElement = { elementId: 'promise-element' };
202
+ const optionsWithPromiseElement = {
203
+ ...baseOptions,
204
+ autoElementScroll: true,
205
+ element: Promise.resolve(resolvedElement),
206
+ };
207
+ executeMock.mockResolvedValueOnce(100);
208
+ await takeElementScreenshot(browserInstance, optionsWithPromiseElement, false);
209
+ // The resolved element (not the Promise) must be passed to browser.execute()
210
+ expect(executeMock.mock.calls[0][1]).toEqual(resolvedElement);
211
+ // And also passed to takeWebElementScreenshot
212
+ expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ element: resolvedElement }));
213
+ });
187
214
  it('should enable fallback when resizeDimensions is provided', async () => {
188
215
  const optionsWithResize = {
189
216
  ...baseOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdio/image-comparison-core",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
4
  "author": "Wim Selles - wswebcreation",
5
5
  "description": "Image comparison core module for @wdio/visual-service - WebdriverIO visual testing framework",
6
6
  "keywords": [
@@ -28,10 +28,10 @@
28
28
  "dependencies": {
29
29
  "jimp": "^1.6.0",
30
30
  "@wdio/logger": "^9.18.0",
31
- "@wdio/types": "^9.20.0"
31
+ "@wdio/types": "^9.25.0"
32
32
  },
33
33
  "devDependencies": {
34
- "webdriverio": "^9.23.0"
34
+ "webdriverio": "^9.25.0"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"