@applitools/screenshoter 3.2.8 → 3.3.2

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 (80) hide show
  1. package/.bongo/dry-run/package-lock.json +49 -6
  2. package/.bongo/dry-run/package.json +5 -0
  3. package/.bongo/dry-run.tgz +0 -0
  4. package/CHANGELOG.md +19 -0
  5. package/index.js +2 -1
  6. package/package.json +7 -4
  7. package/src/find-image-pattern.js +11 -38
  8. package/src/image.js +107 -64
  9. package/src/take-screenshot.js +136 -160
  10. package/src/take-simple-screenshot.js +25 -0
  11. package/src/take-stitched-screenshot.js +34 -40
  12. package/src/take-viewport-screenshot.js +179 -16
  13. package/test/e2e/android.spec.js +120 -13
  14. package/test/e2e/external.spec.js +155 -0
  15. package/test/e2e/ios.spec.js +142 -12
  16. package/test/e2e/web-ios.spec.js +19 -6
  17. package/test/e2e/web.spec.js +33 -21
  18. package/test/fixtures/android/app-fully-non-scrollable.png +0 -0
  19. package/test/fixtures/android/app-fully-recycler.png +0 -0
  20. package/test/fixtures/android/app-fully-scroll-statusbar.png +0 -0
  21. package/test/fixtures/android/app-fully-scroll.png +0 -0
  22. package/test/fixtures/android/app-statusbar.png +0 -0
  23. package/test/fixtures/android/x-app-fully-collapsing.png +0 -0
  24. package/test/fixtures/android/x-app-fully-recycler.png +0 -0
  25. package/test/fixtures/android/x-element-fully.png +0 -0
  26. package/test/fixtures/external/agl.png +0 -0
  27. package/test/fixtures/image/{house.combined-higher-wider.png → house.framed-higher-wider.png} +0 -0
  28. package/test/fixtures/image/{house.combined-higher.png → house.framed-higher.png} +0 -0
  29. package/test/fixtures/image/house.framed-shorter-thinner.png +0 -0
  30. package/test/fixtures/image/{house.combined-wider.png → house.framed-wider.png} +0 -0
  31. package/test/fixtures/ios/app-fully-collapsing.png +0 -0
  32. package/test/fixtures/ios/app-fully-collection.png +0 -0
  33. package/test/fixtures/ios/app-fully-overlapped-statusbar.png +0 -0
  34. package/test/fixtures/ios/app-fully-overlapped.png +0 -0
  35. package/test/fixtures/ios/app-fully-scroll-statusbar.png +0 -0
  36. package/test/fixtures/ios/app-fully-scroll.png +0 -0
  37. package/test/fixtures/ios/app-fully-superview.png +0 -0
  38. package/test/fixtures/ios/app-fully-table.png +0 -0
  39. package/test/fixtures/ios/app-statusbar.png +0 -0
  40. package/test/fixtures/ios/app.png +0 -0
  41. package/test/fixtures/ios/element-fully.png +0 -0
  42. package/test/fixtures/ios/element.png +0 -0
  43. package/test/fixtures/ios/region.png +0 -0
  44. package/test/fixtures/ios/webview-fully.png +0 -0
  45. package/test/fixtures/ios/webview.png +0 -0
  46. package/test/fixtures/pattern/iPad_5th_landscape.png +0 -0
  47. package/test/fixtures/pattern/iPad_5th_portrait.png +0 -0
  48. package/test/fixtures/pattern/iPad_9th_landscape.png +0 -0
  49. package/test/fixtures/pattern/iPad_9th_portrait.png +0 -0
  50. package/test/fixtures/pattern/iPhone_11_landscape.png +0 -0
  51. package/test/fixtures/pattern/iPhone_11_portrait.png +0 -0
  52. package/test/fixtures/pattern/iPhone_13_landscape.png +0 -0
  53. package/test/fixtures/pattern/iPhone_13_portrait.png +0 -0
  54. package/test/fixtures/pattern/iPhone_SE_landscape.png +0 -0
  55. package/test/fixtures/pattern/iPhone_SE_portrait.png +0 -0
  56. package/test/fixtures/pattern/iPhone_XS_portrait_noviewport.png +0 -0
  57. package/test/fixtures/web/frame-fully.png +0 -0
  58. package/test/fixtures/web/frame.png +0 -0
  59. package/test/fixtures/web/inner-element-fully.png +0 -0
  60. package/test/fixtures/web/inner-element.png +0 -0
  61. package/test/fixtures/web/inner-region-fully.png +0 -0
  62. package/test/fixtures/web/inner-region.png +0 -0
  63. package/test/fixtures/web/page-fully.png +0 -0
  64. package/test/fixtures/web/page.png +0 -0
  65. package/test/fixtures/web/region-fully.png +0 -0
  66. package/test/fixtures/web/region.png +0 -0
  67. package/test/fixtures/web-ios/page-fully.png +0 -0
  68. package/test/it/find-pattern.spec.js +16 -11
  69. package/test/it/image.spec.js +42 -15
  70. package/docker-compose.yaml +0 -29
  71. package/src/calculate-screenshot-regions.js +0 -31
  72. package/src/screenshoter.js +0 -158
  73. package/test/fixtures/pattern/iPad_Air_portrait.png +0 -0
  74. package/test/fixtures/pattern/iPhone_5S_landscape.png +0 -0
  75. package/test/fixtures/pattern/iPhone_XR_perfecto_landscape.png +0 -0
  76. package/test/fixtures/pattern/iPhone_XS_Max_perfecto_landscape.png +0 -0
  77. package/test/fixtures/pattern/iPhone_XS_landscape.png +0 -0
  78. package/test/fixtures/pattern/iPhone_XS_portrait.png +0 -0
  79. package/test/fixtures/pattern/iPhone_X_perfecto_portrait.png +0 -0
  80. package/test/util/spec-driver.js +0 -288
@@ -1,183 +1,159 @@
1
1
  const utils = require('@applitools/utils')
2
- const snippets = require('@applitools/snippets')
3
- const findImagePattern = require('./find-image-pattern')
4
- const makeImage = require('./image')
5
-
6
- function makeTakeScreenshot(options) {
7
- const {driver} = options
8
- if (driver.isNative) {
9
- return makeTakeNativeScreenshot(options)
10
- } else if (driver.browserName === 'Firefox') {
11
- try {
12
- const browserVersion = Number.parseInt(driver.browserVersion, 10)
13
- if (browserVersion >= 48 && browserVersion <= 72) {
14
- return makeTakeMainContextScreenshot(options)
15
- }
16
- } catch (ignored) {}
17
- } else if (driver.browserName === 'Safari') {
18
- if (driver.isIOS) {
19
- return makeTakeMarkedScreenshot(options)
20
- } else if (driver.browserVersion === '11') {
21
- return makeTakeSafari11Screenshot(options)
2
+ const makeScroller = require('./scroller')
3
+ const scrollIntoViewport = require('./scroll-into-viewport')
4
+ const takeStitchedScreenshot = require('./take-stitched-screenshot')
5
+ const takeSimpleScreenshot = require('./take-simple-screenshot')
6
+
7
+ async function takeScreenshot({
8
+ driver,
9
+ frames = [],
10
+ region,
11
+ fully,
12
+ scrollingMode,
13
+ hideScrollbars,
14
+ hideCaret,
15
+ withStatusBar,
16
+ overlap,
17
+ framed,
18
+ wait,
19
+ stabilization,
20
+ hooks,
21
+ debug,
22
+ logger,
23
+ }) {
24
+ // screenshot of a window/app was requested (fully or viewport)
25
+ const window = !region && (!frames || frames.length === 0)
26
+ // framed screenshots could be taken only when screenshot of window/app fully was requested
27
+ framed = framed && fully && window
28
+ // screenshots with status bar could be taken only when screenshot of app or framed app fully was requested
29
+ withStatusBar = withStatusBar && driver.isNative && window && (!fully || framed)
30
+ scrollingMode = driver.isNative ? 'scroll' : scrollingMode
31
+
32
+ const activeContext = driver.currentContext
33
+ const context =
34
+ frames.length > 0
35
+ ? await activeContext.context(frames.reduce((parent, frame) => ({...frame, parent}), null))
36
+ : activeContext
37
+
38
+ // traverse from main context to target context to hide scrollbars and preserve context state (scroll/translate position)
39
+ for (const nextContext of context.path) {
40
+ const scrollingElement = await nextContext.getScrollingElement()
41
+ // unlike web apps, native apps do not always have scrolling element
42
+ if (scrollingElement) {
43
+ if (driver.isWeb && hideScrollbars) await scrollingElement.hideScrollbars()
44
+ await scrollingElement.preserveState()
22
45
  }
23
46
  }
24
47
 
25
- return makeTakeDefaultScreenshot(options)
26
- }
27
-
28
- function makeTakeDefaultScreenshot({driver, stabilization = {}, debug, logger}) {
29
- const calculateScaleRatio = makeCalculateScaleRatio({driver})
30
- return async function takeScreenshot({name} = {}) {
31
- logger.verbose('Taking screenshot...')
32
- const image = makeImage(await driver.takeScreenshot())
33
- await image.debug({...debug, name, suffix: 'original'})
48
+ // blur active element in target context
49
+ const activeElement = driver.isWeb && hideCaret ? await context.blurElement() : null
34
50
 
35
- if (stabilization.scale) image.scale(stabilization.scale)
36
- else image.scale(await calculateScaleRatio(image.width))
51
+ const target = await getTarget({window, context, region, fully, scrollingMode, logger})
37
52
 
38
- if (stabilization.rotate) image.crop(stabilization.rotate)
53
+ if (driver.isWeb && hideScrollbars) await target.scroller.hideScrollbars()
39
54
 
40
- if (stabilization.crop) image.crop(stabilization.crop)
41
-
42
- return image
43
- }
44
- }
55
+ try {
56
+ if (!window) await scrollIntoViewport({...target, logger})
45
57
 
46
- function makeTakeMainContextScreenshot({driver, stabilization = {}, debug, logger}) {
47
- const calculateScaleRatio = makeCalculateScaleRatio({driver})
48
- return async function takeScreenshot({name} = {}) {
49
- logger.verbose('Taking screenshot...')
50
- const originalContext = driver.currentContext
51
- await driver.mainContext.focus()
52
- const image = makeImage(await driver.takeScreenshot())
53
- await originalContext.focus()
54
- await image.debug({...debug, name, suffix: 'original'})
58
+ const screenshot =
59
+ fully && target.scroller
60
+ ? await takeStitchedScreenshot({...target, withStatusBar, overlap, framed, wait, stabilization, debug, logger})
61
+ : await takeSimpleScreenshot({...target, withStatusBar, wait, stabilization, debug, logger})
55
62
 
56
- if (stabilization.scale) image.scale(stabilization.scale)
57
- else image.scale(await calculateScaleRatio(image.width))
58
-
59
- if (stabilization.rotate) image.rotate(stabilization.rotate)
60
-
61
- if (stabilization.crop) image.crop(stabilization.crop)
62
-
63
- return image
64
- }
65
- }
66
-
67
- function makeTakeSafari11Screenshot({driver, stabilization = {}, debug, logger}) {
68
- const calculateScaleRatio = makeCalculateScaleRatio({driver})
69
- let viewportSize
70
-
71
- return async function takeScreenshot({name} = {}) {
72
- logger.verbose('Taking safari 11 driver screenshot...')
73
- const image = makeImage(await driver.takeScreenshot())
74
- await image.debug({...debug, name, suffix: 'original'})
75
-
76
- if (stabilization.scale) image.scale(stabilization.scale)
77
- else image.scale(await calculateScaleRatio(image.width))
78
-
79
- if (stabilization.rotate) image.rotate(stabilization.rotate)
80
-
81
- if (stabilization.crop) image.crop(stabilization.crop)
82
- else {
83
- if (!viewportSize) viewportSize = await driver.getViewportSize()
84
- const viewportLocation = await driver.mainContext.execute(snippets.getElementScrollOffset, [])
85
- image.crop(utils.geometry.region(viewportLocation, viewportSize))
63
+ if (hooks && hooks.afterScreenshot) {
64
+ // imitate image-like state for the hook
65
+ if (window && fully && target.scroller) {
66
+ await target.scroller.moveTo({x: 0, y: 0}, await driver.mainContext.getScrollingElement())
67
+ }
68
+ await hooks.afterScreenshot({driver, scroller: target.scroller, screenshot})
86
69
  }
87
70
 
88
- return image
89
- }
90
- }
91
-
92
- function makeTakeMarkedScreenshot({driver, stabilization = {}, debug, logger}) {
93
- const calculateScaleRatio = makeCalculateScaleRatio({driver})
94
- let viewportRegion
95
-
96
- return async function takeScreenshot({name} = {}) {
97
- logger.verbose('Taking viewport screenshot (using markers)...')
98
- const image = makeImage(await driver.takeScreenshot())
99
- await image.debug({...debug, name, suffix: 'original'})
100
-
101
- if (stabilization.scale) image.scale(stabilization.scale)
102
- else image.scale(await calculateScaleRatio(image.width))
103
-
104
- if (stabilization.rotate) image.rotate(stabilization.rotate)
105
-
106
- if (stabilization.crop) image.crop(stabilization.crop)
107
- else {
108
- if (!viewportRegion) viewportRegion = await getViewportRegion()
109
- image.crop(viewportRegion)
110
- await image.debug({...debug, name, suffix: 'viewport'})
71
+ return screenshot
72
+ } finally {
73
+ if (target.scroller) {
74
+ await target.scroller.restoreScrollbars()
111
75
  }
112
76
 
113
- return image
114
- }
115
-
116
- async function getViewportRegion() {
117
- const marker = await driver.mainContext.execute(snippets.addPageMarker)
118
- try {
119
- const image = makeImage(await driver.takeScreenshot())
120
-
121
- if (stabilization.rotate) await image.rotate(stabilization.rotate)
77
+ // if there was active element and we have blurred it, then restore focus
78
+ if (activeElement) await context.focusElement(activeElement)
122
79
 
123
- await image.debug({...debug, name: 'marker'})
124
-
125
- const markerLocation = findImagePattern(await image.toObject(), marker)
126
- if (!markerLocation) return null
127
-
128
- const viewportSize = await driver.getViewportSize()
129
-
130
- return utils.geometry.region(utils.geometry.scale(markerLocation, 1 / driver.pixelRatio), viewportSize)
131
- } finally {
132
- await driver.mainContext.execute(snippets.cleanupPageMarker)
133
- }
134
- }
135
- }
136
-
137
- function makeTakeNativeScreenshot({driver, stabilization = {}, debug, logger}) {
138
- return async function takeScreenshot({name, withStatusBar} = {}) {
139
- logger.verbose('Taking native driver screenshot...')
140
- const image = makeImage(await driver.takeScreenshot())
141
- await image.debug({...debug, name, suffix: 'original'})
142
-
143
- if (stabilization.scale) image.scale(stabilization.scale)
144
- else image.scale(1 / driver.pixelRatio)
145
-
146
- if (stabilization.rotate) image.rotate(stabilization.rotate)
147
-
148
- if (stabilization.crop) image.crop(stabilization.crop)
149
- else {
150
- const viewportSize = await driver.getViewportSize()
151
- const cropRegion = withStatusBar
152
- ? {x: 0, y: 0, width: viewportSize.width, height: viewportSize.height + driver.statusBarHeight}
153
- : {x: 0, y: driver.statusBarHeight, width: viewportSize.width, height: viewportSize.height}
154
- image.crop(cropRegion)
155
- await image.debug({...debug, name, suffix: `viewport${withStatusBar ? '-with-statusbar' : ''}`})
80
+ // traverse from target context to the main context to restore scrollbars and context states
81
+ for (const prevContext of context.path.reverse()) {
82
+ const scrollingElement = await prevContext.getScrollingElement()
83
+ if (scrollingElement) {
84
+ if (driver.isWeb && hideScrollbars) await scrollingElement.restoreScrollbars()
85
+ await scrollingElement.restoreState()
86
+ }
156
87
  }
157
88
 
158
- return image
89
+ // restore focus on original active context
90
+ await activeContext.focus()
159
91
  }
160
92
  }
161
93
 
162
- function makeCalculateScaleRatio({driver}) {
163
- let viewportWidth, contentWidth
164
- const VIEWPORT_THRESHOLD = 1
165
- const CONTENT_THRESHOLD = 10
166
- return async function calculateScaleRatio(imageWidth) {
167
- if (!viewportWidth) viewportWidth = await driver.getViewportSize().then(size => size.width)
168
- if (!contentWidth) contentWidth = await driver.mainContext.getContentSize().then(size => size.width)
169
- // If the image's width is the same as the viewport's width or the
170
- // top level context's width, no scaling is necessary.
171
- if (
172
- (imageWidth >= viewportWidth - VIEWPORT_THRESHOLD && imageWidth <= viewportWidth + VIEWPORT_THRESHOLD) ||
173
- (imageWidth >= contentWidth - CONTENT_THRESHOLD && imageWidth <= contentWidth + CONTENT_THRESHOLD)
174
- ) {
175
- return 1
94
+ async function getTarget({window, context, region, fully, scrollingMode, logger}) {
95
+ if (window) {
96
+ // window/app
97
+ const scrollingElement = await context.main.getScrollingElement()
98
+ return {
99
+ context: context.main,
100
+ scroller: scrollingElement ? makeScroller({element: scrollingElement, scrollingMode, logger}) : null,
101
+ }
102
+ } else if (region) {
103
+ if (utils.types.has(region, ['x', 'y', 'width', 'height'])) {
104
+ // region by coordinates
105
+ const scrollingElement = await context.getScrollingElement()
106
+ return {
107
+ context,
108
+ region,
109
+ scroller: scrollingElement ? makeScroller({element: scrollingElement, scrollingMode, logger}) : null,
110
+ }
111
+ } else {
112
+ // region by element or selector
113
+ const element = await context.element(region)
114
+ if (!element) throw new Error('Element not found!')
115
+
116
+ const elementContext = element.context
117
+
118
+ if (fully) {
119
+ const isScrollable = await element.isScrollable()
120
+ // if element is scrollable, then take screenshot of the full element content, otherwise take screenshot of full element
121
+ const region = isScrollable ? null : await element.getRegion()
122
+ const scrollingElement = isScrollable ? element : await elementContext.getScrollingElement()
123
+ // css stitching could be applied only to root element of its context
124
+ scrollingMode = scrollingMode === 'css' && !(await scrollingElement.isRoot()) ? 'mixed' : scrollingMode
125
+ return {
126
+ context: elementContext,
127
+ region,
128
+ scroller: scrollingElement ? makeScroller({element: scrollingElement, scrollingMode, logger}) : null,
129
+ }
130
+ } else {
131
+ const scrollingElement = await context.getScrollingElement()
132
+ return {
133
+ context: elementContext,
134
+ region: await element.getRegion(),
135
+ scroller: scrollingElement ? makeScroller({element: scrollingElement, scrollingMode, logger}) : null,
136
+ }
137
+ }
138
+ }
139
+ } else if (!context.isMain) {
140
+ // context
141
+ if (fully) {
142
+ const scrollingElement = await context.getScrollingElement()
143
+ return {
144
+ context,
145
+ scroller: scrollingElement ? makeScroller({logger, element: scrollingElement, scrollingMode}) : null,
146
+ }
147
+ } else {
148
+ const scrollingElement = await context.parent.getScrollingElement()
149
+ const element = await context.getContextElement()
150
+ return {
151
+ context: context.parent,
152
+ region: await element.getRegion(), // IMHO we should use CLIENT (without borders) region here
153
+ scroller: scrollingElement ? makeScroller({logger, element: scrollingElement, scrollingMode}) : null,
154
+ }
176
155
  }
177
-
178
- const scaledImageWidth = Math.round(imageWidth / driver.pixelRatio)
179
- return viewportWidth / scaledImageWidth / driver.pixelRatio
180
156
  }
181
157
  }
182
158
 
183
- module.exports = makeTakeScreenshot
159
+ module.exports = takeScreenshot
@@ -0,0 +1,25 @@
1
+ const utils = require('@applitools/utils')
2
+ const makeTakeViewportScreenshot = require('./take-viewport-screenshot')
3
+
4
+ async function takeSimpleScreenshot({context, region, withStatusBar, wait, stabilization, debug = {}, logger}) {
5
+ logger.verbose('Taking image of...')
6
+
7
+ const driver = context.driver
8
+ const takeViewportScreenshot = makeTakeViewportScreenshot({logger, driver, stabilization, debug})
9
+
10
+ await utils.general.sleep(wait)
11
+
12
+ const image = await takeViewportScreenshot({withStatusBar})
13
+
14
+ if (region) {
15
+ const cropRegion = await driver.getRegionInViewport(context, region)
16
+ if (utils.geometry.isEmpty(cropRegion)) throw new Error('Screenshot region is out of viewport')
17
+ image.crop(cropRegion)
18
+ await image.debug({path: debug.path, suffix: 'region'})
19
+ return {image, region: cropRegion}
20
+ } else {
21
+ return {image, region: utils.geometry.region({x: 0, y: 0}, image.size)}
22
+ }
23
+ }
24
+
25
+ module.exports = takeSimpleScreenshot
@@ -1,6 +1,6 @@
1
1
  const utils = require('@applitools/utils')
2
2
  const makeImage = require('./image')
3
- const makeTakeScreenshot = require('./take-screenshot')
3
+ const makeTakeViewportScreenshot = require('./take-viewport-screenshot')
4
4
 
5
5
  async function takeStitchedScreenshot({
6
6
  logger,
@@ -8,7 +8,7 @@ async function takeStitchedScreenshot({
8
8
  scroller,
9
9
  region,
10
10
  withStatusBar,
11
- overlap = 50,
11
+ overlap = {top: 10, bottom: 50},
12
12
  framed,
13
13
  wait,
14
14
  stabilization,
@@ -17,7 +17,7 @@ async function takeStitchedScreenshot({
17
17
  logger.verbose('Taking full image of...')
18
18
 
19
19
  const driver = context.driver
20
- const takeScreenshot = makeTakeScreenshot({logger, driver, stabilization, debug})
20
+ const takeViewportScreenshot = makeTakeViewportScreenshot({logger, driver, stabilization, debug})
21
21
  const scrollerState = await scroller.preserveState()
22
22
 
23
23
  const initialOffset = region ? utils.geometry.location(region) : {x: 0, y: 0}
@@ -26,26 +26,26 @@ async function takeStitchedScreenshot({
26
26
 
27
27
  await utils.general.sleep(wait)
28
28
 
29
+ const contentSize = await scroller.getContentSize()
30
+
29
31
  logger.verbose('Getting initial image...')
30
- let image = await takeScreenshot({name: 'initial', withStatusBar})
32
+ let image = await takeViewportScreenshot({name: 'initial', withStatusBar})
31
33
  const firstImage = framed ? makeImage(image) : null
32
34
 
35
+ const scrollerRegion = await scroller.getClientRegion()
33
36
  const targetRegion = region
34
- ? utils.geometry.intersect(
35
- utils.geometry.region(await scroller.getInnerOffset(), await scroller.getClientRegion()),
36
- region,
37
- )
38
- : await scroller.getClientRegion()
37
+ ? utils.geometry.intersect(utils.geometry.region(await scroller.getInnerOffset(), scrollerRegion), region)
38
+ : scrollerRegion
39
39
 
40
40
  // TODO the solution should not check driver specifics,
41
41
  // in this case target region coordinate should be already related to the scrolling element of the context
42
- const cropRegion = driver.isNative ? targetRegion : await driver.getRegionInViewport(context, targetRegion)
42
+ let cropRegion = driver.isNative ? targetRegion : await driver.getRegionInViewport(context, targetRegion)
43
43
 
44
44
  logger.verbose('cropping...')
45
45
  image.crop(withStatusBar ? utils.geometry.offset(cropRegion, {x: 0, y: driver.statusBarHeight}) : cropRegion)
46
46
  await image.debug({...debug, name: 'initial', suffix: 'region'})
47
47
 
48
- const contentRegion = utils.geometry.region({x: 0, y: 0}, await scroller.getContentSize())
48
+ const contentRegion = utils.geometry.region({x: 0, y: 0}, contentSize)
49
49
  logger.verbose(`Scroller size: ${contentRegion}`)
50
50
 
51
51
  if (region) region = utils.geometry.intersect(region, contentRegion)
@@ -53,37 +53,41 @@ async function takeStitchedScreenshot({
53
53
 
54
54
  region = utils.geometry.round(region)
55
55
 
56
- // TODO padding should be provided from args instead of overlap
57
- const padding = {top: overlap, bottom: overlap}
58
- const [initialRegion, ...partRegions] = utils.geometry.divide(region, utils.geometry.round(image.size), padding)
56
+ const [initialRegion, ...partRegions] = utils.geometry.divide(region, image.size, overlap)
59
57
  logger.verbose('Part regions', partRegions)
60
58
 
61
59
  logger.verbose('Creating stitched image composition container')
62
- const stitchedImage = makeImage({width: region.width, height: region.height})
60
+ const stitchedImage = makeImage({auto: true})
63
61
 
64
62
  logger.verbose('Adding initial image...')
65
- await stitchedImage.copy(image, {x: 0, y: 0})
63
+ stitchedImage.copy(image, {x: 0, y: 0})
66
64
 
67
65
  logger.verbose('Getting the rest of the image parts...')
68
66
 
69
- let stitchedSize = {width: image.width, height: image.height}
70
- let lastImage
67
+ let lastImage = firstImage
68
+ let scrollerRegionShift = {x: 0, y: 0}
71
69
  for (const partRegion of partRegions) {
72
70
  const partName = `${partRegion.x}_${partRegion.y}_${partRegion.width}x${partRegion.height}`
73
71
  logger.verbose(`Processing part ${partName}`)
74
72
 
75
- const compensateOffset = {x: 0, y: initialRegion.y !== partRegion.y ? padding.top : 0}
73
+ const compensateOffset = {x: 0, y: initialRegion.y !== partRegion.y ? overlap.top : 0}
76
74
  const requiredOffset = utils.geometry.offsetNegative(utils.geometry.location(partRegion), compensateOffset)
77
75
 
78
76
  logger.verbose(`Move to ${requiredOffset}`)
79
- const actualOffset = await scroller.moveTo(requiredOffset)
80
- const remainingOffset = utils.geometry.offset(
81
- utils.geometry.offsetNegative(
82
- utils.geometry.offsetNegative(requiredOffset, actualOffset),
83
- expectedRemainingOffset,
84
- ),
85
- compensateOffset,
86
- )
77
+ let actualOffset = await scroller.moveTo(requiredOffset)
78
+ // actual scroll position after scrolling might be not equal to required position due to
79
+ // scrollable region shift during scrolling so actual scroll position should be corrected
80
+ if (!utils.geometry.equals(actualOffset, requiredOffset) && driver.isNative) {
81
+ const actualScrollerRegion = await scroller.getClientRegion()
82
+ scrollerRegionShift = {x: scrollerRegion.x - actualScrollerRegion.x, y: scrollerRegion.y - actualScrollerRegion.y}
83
+ }
84
+ actualOffset = utils.geometry.offset(actualOffset, scrollerRegionShift)
85
+
86
+ const remainingOffset = {
87
+ x: requiredOffset.x - actualOffset.x - expectedRemainingOffset.x + compensateOffset.x,
88
+ y: requiredOffset.y - actualOffset.y - expectedRemainingOffset.y + compensateOffset.y,
89
+ }
90
+
87
91
  const cropPartRegion = {
88
92
  x: cropRegion.x + remainingOffset.x,
89
93
  y: cropRegion.y + remainingOffset.y,
@@ -97,7 +101,7 @@ async function takeStitchedScreenshot({
97
101
  if (utils.geometry.isEmpty(cropPartRegion) || !utils.geometry.isIntersected(cropRegion, cropPartRegion)) continue
98
102
 
99
103
  logger.verbose('Getting image...')
100
- image = await takeScreenshot({name: partName})
104
+ image = await takeViewportScreenshot({name: partName})
101
105
  lastImage = framed ? makeImage(image) : null
102
106
 
103
107
  logger.verbose('cropping...')
@@ -105,25 +109,15 @@ async function takeStitchedScreenshot({
105
109
  await image.debug({...debug, name: partName, suffix: 'region'})
106
110
 
107
111
  const pasteOffset = utils.geometry.offsetNegative(utils.geometry.location(partRegion), initialOffset)
108
- await stitchedImage.copy(image, pasteOffset)
109
-
110
- stitchedSize = {width: pasteOffset.x + image.width, height: pasteOffset.y + image.height}
112
+ stitchedImage.copy(image, pasteOffset)
111
113
  }
112
114
 
113
115
  await scroller.restoreState(scrollerState)
114
116
 
115
- logger.verbose(`Extracted entire size: ${region}`)
116
- logger.verbose(`Actual stitched size: ${stitchedSize}`)
117
-
118
- if (stitchedSize.width < stitchedImage.width || stitchedSize.height < stitchedImage.height) {
119
- logger.verbose('Trimming unnecessary margins...')
120
- stitchedImage.crop(utils.geometry.region({x: 0, y: 0}, stitchedSize))
121
- }
122
-
123
117
  await stitchedImage.debug({...debug, name: 'stitched'})
124
118
 
125
119
  if (framed) {
126
- await stitchedImage.combine(
120
+ stitchedImage.frame(
127
121
  firstImage,
128
122
  lastImage,
129
123
  withStatusBar ? utils.geometry.offset(cropRegion, {x: 0, y: driver.statusBarHeight}) : cropRegion,