@applitools/screenshoter 3.2.9 → 3.3.3

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 (97) hide show
  1. package/.bongo/dry-run/package-lock.json +19 -19
  2. package/.bongo/dry-run.tgz +0 -0
  3. package/CHANGELOG.md +23 -0
  4. package/docker-compose.yaml +29 -0
  5. package/index.js +2 -1
  6. package/package.json +6 -6
  7. package/src/find-image-pattern.js +11 -38
  8. package/src/image.js +116 -73
  9. package/src/scroll-into-viewport.js +16 -7
  10. package/src/take-screenshot.js +136 -160
  11. package/src/take-simple-screenshot.js +25 -0
  12. package/src/take-stitched-screenshot.js +32 -52
  13. package/src/take-viewport-screenshot.js +157 -16
  14. package/test/e2e/android.spec.js +84 -11
  15. package/test/e2e/external.spec.js +81 -10
  16. package/test/e2e/ios.spec.js +129 -13
  17. package/test/e2e/web-ios.spec.js +44 -11
  18. package/test/e2e/web.spec.js +20 -15
  19. package/test/fixtures/android/app-fully-non-scrollable.png +0 -0
  20. package/test/fixtures/android/app-fully-recycler.png +0 -0
  21. package/test/fixtures/android/app-fully-scroll-statusbar.png +0 -0
  22. package/test/fixtures/android/app-fully-scroll.png +0 -0
  23. package/test/fixtures/android/app-statusbar.png +0 -0
  24. package/test/fixtures/android/x-app-fully-collapsing.png +0 -0
  25. package/test/fixtures/android/x-app-fully-recycler.png +0 -0
  26. package/test/fixtures/android/x-element-fully.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-ios/page-fully-landscape.png +0 -0
  58. package/test/fixtures/web-ios/page-fully.png +0 -0
  59. package/test/fixtures/web-ios/page-landscape.png +0 -0
  60. package/test/it/find-pattern.spec.js +16 -11
  61. package/test/it/image.spec.js +42 -15
  62. package/logs/screenshot_2021_11_14_12_35_00_342Z_full_frame_failed.png +0 -0
  63. package/logs/screenshot_2021_11_14_12_38_00_715Z_frame_failed.png +0 -0
  64. package/logs/screenshot_2021_11_14_12_38_03_866Z_frame_failed.png +0 -0
  65. package/logs/screenshot_2021_11_14_13_02_56_464Z_full_app_failed_1636894976464.png +0 -0
  66. package/logs/screenshot_2021_11_14_13_04_27_904Z_full_app_failed_1636895067904.png +0 -0
  67. package/logs/screenshot_2021_11_14_13_06_13_662Z_full_app_failed_1636895173662.png +0 -0
  68. package/logs/screenshot_2021_11_14_13_06_23_745Z_full_app_failed_1636895183745.png +0 -0
  69. package/logs/screenshot_2021_11_14_13_18_31_571Z_full_app_failed_1636895911571.png +0 -0
  70. package/logs/screenshot_2021_11_14_13_25_54_557Z_viewport_failed_1636896354557.png +0 -0
  71. package/logs/screenshot_2021_11_14_13_29_32_326Z_viewport_failed_1636896572326.png +0 -0
  72. package/logs/screenshot_2021_11_14_13_34_22_483Z_viewport_failed_1636896862483.png +0 -0
  73. package/logs/screenshot_2021_11_14_13_37_25_734Z_viewport_failed_1636897045734.png +0 -0
  74. package/logs/screenshot_2021_11_14_13_42_25_024Z_viewport_failed_1636897345024.png +0 -0
  75. package/logs/screenshot_2021_11_14_13_57_24_366Z_full_app_failed_1636898244366.png +0 -0
  76. package/logs/screenshot_2021_11_14_14_20_42_951Z_full_app_failed_1636899642951.png +0 -0
  77. package/logs/screenshot_2021_11_14_14_31_07_853Z_full_app_failed_1636900267853.png +0 -0
  78. package/logs/screenshot_2021_11_14_14_32_07_195Z_full_app_failed_1636900327195.png +0 -0
  79. package/logs/screenshot_2021_11_14_14_42_16_716Z_full_app_failed_1636900936716.png +0 -0
  80. package/logs/screenshot_2021_11_14_14_47_37_646Z_full_app_failed_1636901257646.png +0 -0
  81. package/logs/screenshot_2021_11_14_14_54_18_522Z_full_app_failed_1636901658522.png +0 -0
  82. package/logs/screenshot_2021_11_14_14_55_36_756Z_full_app_failed_1636901736756.png +0 -0
  83. package/logs/screenshot_2021_11_14_15_00_26_000Z_full_app_failed_1636902026000.png +0 -0
  84. package/logs/screenshot_2021_11_14_15_04_13_598Z_full_app_failed_1636902253598.png +0 -0
  85. package/logs/screenshot_2021_11_14_15_07_37_914Z_full_app_failed_1636902457914.png +0 -0
  86. package/logs/screenshot_2021_11_14_15_12_20_039Z_full_app_failed_1636902740039.png +0 -0
  87. package/logs/screenshot_2021_11_14_15_15_44_401Z_full_app_failed_1636902944401.png +0 -0
  88. package/logs/screenshot_2021_11_14_15_26_23_318Z_viewport_failed_1636903583318.png +0 -0
  89. package/src/calculate-screenshot-regions.js +0 -31
  90. package/src/screenshoter.js +0 -159
  91. package/test/fixtures/pattern/iPad_Air_portrait.png +0 -0
  92. package/test/fixtures/pattern/iPhone_5S_landscape.png +0 -0
  93. package/test/fixtures/pattern/iPhone_XR_perfecto_landscape.png +0 -0
  94. package/test/fixtures/pattern/iPhone_XS_Max_perfecto_landscape.png +0 -0
  95. package/test/fixtures/pattern/iPhone_XS_landscape.png +0 -0
  96. package/test/fixtures/pattern/iPhone_XS_portrait.png +0 -0
  97. package/test/fixtures/pattern/iPhone_X_perfecto_portrait.png +0 -0
@@ -9,13 +9,13 @@
9
9
  }
10
10
  },
11
11
  "node_modules/@applitools/screenshoter": {
12
- "version": "3.2.8",
12
+ "version": "3.3.2",
13
13
  "resolved": "file:../dry-run.tgz",
14
- "integrity": "sha512-GjN+h/vj5/lKFjJPm302jD0xSFIOoSG7gPU/ogEUcCS9l5FDcMW27PRMA/xGlolKrabmCgDHJqVbAXE1WCgjyQ==",
14
+ "integrity": "sha512-5epXLfRKO+64jIbxTYwUMUui0xGPQmwccz5aMlLg2V1gtgyrcZ689cn7n8jDBWPCt1Ds0MNEZCimcDjGiAXb4Q==",
15
15
  "license": "SEE LICENSE IN LICENSE",
16
16
  "dependencies": {
17
- "@applitools/snippets": "2.1.7",
18
- "@applitools/utils": "1.2.4",
17
+ "@applitools/snippets": "2.1.11",
18
+ "@applitools/utils": "1.2.5",
19
19
  "png-async": "0.9.4"
20
20
  },
21
21
  "engines": {
@@ -23,17 +23,17 @@
23
23
  }
24
24
  },
25
25
  "node_modules/@applitools/snippets": {
26
- "version": "2.1.7",
27
- "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.7.tgz",
28
- "integrity": "sha512-Tr4Gj7Qov/oPy+8WI4oVmmubxqpOzr8P3Wjzpl6rA57xKLg6/TiIg5oZNb4+jEmO2ShjNYLaEwRWHl7kPgb4fw==",
26
+ "version": "2.1.11",
27
+ "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.11.tgz",
28
+ "integrity": "sha512-uNx2sqFACva5Lt23NvYjnxkbUoyAmoCN8dVtAFOhL2a0HyxzYKP5z0tCT/JK8QqM3gkSzeQ0rT0FdxQ9UAl7Og==",
29
29
  "engines": {
30
30
  "node": ">=8.9.0"
31
31
  }
32
32
  },
33
33
  "node_modules/@applitools/utils": {
34
- "version": "1.2.4",
35
- "resolved": "https://registry.npmjs.org/@applitools/utils/-/utils-1.2.4.tgz",
36
- "integrity": "sha512-w7ma6FFGyqhdP6LEcuHFWOcH7EzBjnoAX3UfbFWcTHA3QXnXPX37Y2ENYRodfwkorP1cUKyUHwNXJB/BMIj/hg==",
34
+ "version": "1.2.5",
35
+ "resolved": "https://registry.npmjs.org/@applitools/utils/-/utils-1.2.5.tgz",
36
+ "integrity": "sha512-nETSgqGeCk5yqjFaQ7x1KURf+t5IxsY2RoeFB5w1+6lHprmHdEithMcN0tiJWeqi14QBJdUkXCzCJrMW5RcDFg==",
37
37
  "engines": {
38
38
  "node": ">= 8.9.0"
39
39
  }
@@ -47,22 +47,22 @@
47
47
  "dependencies": {
48
48
  "@applitools/screenshoter": {
49
49
  "version": "file:../dry-run.tgz",
50
- "integrity": "sha512-GjN+h/vj5/lKFjJPm302jD0xSFIOoSG7gPU/ogEUcCS9l5FDcMW27PRMA/xGlolKrabmCgDHJqVbAXE1WCgjyQ==",
50
+ "integrity": "sha512-5epXLfRKO+64jIbxTYwUMUui0xGPQmwccz5aMlLg2V1gtgyrcZ689cn7n8jDBWPCt1Ds0MNEZCimcDjGiAXb4Q==",
51
51
  "requires": {
52
- "@applitools/snippets": "2.1.7",
53
- "@applitools/utils": "1.2.4",
52
+ "@applitools/snippets": "2.1.11",
53
+ "@applitools/utils": "1.2.5",
54
54
  "png-async": "0.9.4"
55
55
  }
56
56
  },
57
57
  "@applitools/snippets": {
58
- "version": "2.1.7",
59
- "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.7.tgz",
60
- "integrity": "sha512-Tr4Gj7Qov/oPy+8WI4oVmmubxqpOzr8P3Wjzpl6rA57xKLg6/TiIg5oZNb4+jEmO2ShjNYLaEwRWHl7kPgb4fw=="
58
+ "version": "2.1.11",
59
+ "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.1.11.tgz",
60
+ "integrity": "sha512-uNx2sqFACva5Lt23NvYjnxkbUoyAmoCN8dVtAFOhL2a0HyxzYKP5z0tCT/JK8QqM3gkSzeQ0rT0FdxQ9UAl7Og=="
61
61
  },
62
62
  "@applitools/utils": {
63
- "version": "1.2.4",
64
- "resolved": "https://registry.npmjs.org/@applitools/utils/-/utils-1.2.4.tgz",
65
- "integrity": "sha512-w7ma6FFGyqhdP6LEcuHFWOcH7EzBjnoAX3UfbFWcTHA3QXnXPX37Y2ENYRodfwkorP1cUKyUHwNXJB/BMIj/hg=="
63
+ "version": "1.2.5",
64
+ "resolved": "https://registry.npmjs.org/@applitools/utils/-/utils-1.2.5.tgz",
65
+ "integrity": "sha512-nETSgqGeCk5yqjFaQ7x1KURf+t5IxsY2RoeFB5w1+6lHprmHdEithMcN0tiJWeqi14QBJdUkXCzCJrMW5RcDFg=="
66
66
  },
67
67
  "png-async": {
68
68
  "version": "0.9.4",
Binary file
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@
4
4
  ## Unreleased
5
5
 
6
6
 
7
+ ## 3.3.3 - 2021/12/22
8
+
9
+ - improve default rotation and scaling logic
10
+ - improve scroll into viewport algorithm
11
+ - fix lazy handling of image rotation
12
+ - updated to @applitools/snippets@2.1.11 (from 2.1.10)
13
+ - updated to @applitools/utils@1.2.5 (from 1.2.4)
14
+
15
+ ## 3.3.2 - 2021/12/20
16
+
17
+ - add basic support of webview screenshots on ios
18
+ - updated to @applitools/snippets@2.1.10 (from 2.1.8)
19
+
20
+ ## 3.3.1 - 2021/12/17
21
+
22
+ - no changes
23
+
24
+ ## 3.3.0 - 2021/12/16
25
+
26
+ - fix ios web screenshots on pages without viewport meta tag
27
+ - improve native apps support
28
+ - updated to @applitools/snippets@2.1.8 (from 2.1.7)
29
+
7
30
  ## 3.2.9 - 2021/11/14
8
31
 
9
32
  - add support of scrollable elements that change its size during scrolling
@@ -0,0 +1,29 @@
1
+ {
2
+ "version": "3.4",
3
+ "services": {
4
+ "chrome": {
5
+ "image": "selenium/standalone-chrome",
6
+ "environment": [
7
+ "SE_NODE_OVERRIDE_MAX_SESSIONS=true",
8
+ "SE_NODE_MAX_SESSIONS=30"
9
+ ],
10
+ "volumes": [
11
+ "/dev/shm:/dev/shm"
12
+ ],
13
+ "network_mode": "host"
14
+ },
15
+ "firefox": {
16
+ "image": "selenium/standalone-firefox",
17
+ "environment": [
18
+ "SE_NODE_OVERRIDE_MAX_SESSIONS=true",
19
+ "SE_NODE_MAX_SESSIONS=30"
20
+ ],
21
+ "volumes": [
22
+ "/dev/shm:/dev/shm"
23
+ ],
24
+ "ports": [
25
+ "4445:4444"
26
+ ]
27
+ }
28
+ }
29
+ }
package/index.js CHANGED
@@ -1 +1,2 @@
1
- module.exports = require('./src/screenshoter')
1
+ module.exports = require('./src/take-screenshot')
2
+ exports.takeScreenshot = require('./src/take-screenshot')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/screenshoter",
3
- "version": "3.2.9",
3
+ "version": "3.3.3",
4
4
  "description": "Applitools universal screenshoter for web and native applications",
5
5
  "keywords": [
6
6
  "applitools",
@@ -49,15 +49,15 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@applitools/snippets": "2.1.7",
53
- "@applitools/utils": "1.2.4",
52
+ "@applitools/snippets": "2.1.11",
53
+ "@applitools/utils": "1.2.5",
54
54
  "png-async": "0.9.4"
55
55
  },
56
56
  "devDependencies": {
57
- "@applitools/driver": "1.3.2",
57
+ "@applitools/driver": "1.4.6",
58
58
  "@applitools/sdk-release-kit": "0.13.4",
59
- "@applitools/spec-driver-webdriverio": "1.2.0",
60
- "@applitools/test-utils": "1.0.9",
59
+ "@applitools/spec-driver-webdriverio": "1.2.5",
60
+ "@applitools/test-utils": "1.0.10",
61
61
  "chromedriver": "^95.0.0",
62
62
  "eslint": "^7.9.0",
63
63
  "eslint-plugin-mocha-no-only": "^1.1.1",
@@ -1,60 +1,33 @@
1
1
  function findImagePattern(image, pattern) {
2
2
  for (let pixel = 0; pixel < image.width * image.height; ++pixel) {
3
3
  if (isPattern(image, pixel, pattern)) {
4
- return {
5
- x: (pixel % image.width) - pattern.offset,
6
- y: Math.floor(pixel / image.width) - pattern.offset,
7
- }
4
+ const patterOffset = pattern.offset * pattern.pixelRatio
5
+ return {x: (pixel % image.width) - patterOffset, y: Math.floor(pixel / image.width) - patterOffset}
8
6
  }
9
7
  }
10
8
  return null
11
9
  }
12
10
 
13
11
  function isPattern(image, index, pattern) {
14
- const channels = 4
15
- const roundNumber = pattern.size - Math.floor(pattern.size / 2)
16
- for (const [chunkIndex, chunkColor] of pattern.mask.entries()) {
17
- const pixelOffset = index + image.width * pattern.size * chunkIndex
18
- for (let round = 0; round < roundNumber; ++round) {
19
- const sideLength = pattern.size - round * 2
20
- const stepsNumber = sideLength * channels - channels
21
- const threshold = Math.min((roundNumber - round) * 10 + 10, 100)
22
- for (let step = 0; step < stepsNumber; ++step) {
23
- let pixelIndex = pixelOffset + round + round * image.width
24
-
25
- if (step < sideLength) {
26
- pixelIndex += step
27
- } else if (step < sideLength * 2 - 1) {
28
- pixelIndex += sideLength - 1 + ((step % sideLength) + 1) * image.width
29
- } else if (step < sideLength * 3 - 2) {
30
- pixelIndex += (sideLength - 1) * image.width + (sideLength - (step % sideLength) - 1)
31
- } else {
32
- pixelIndex += (step % sideLength) * image.width
33
- }
34
-
35
- const pixelColor = pixelColorAt(image, pixelIndex, threshold)
36
- if (pixelColor !== chunkColor) {
37
- return false
38
- }
39
- }
12
+ const itemLength = pattern.size * pattern.pixelRatio
13
+ for (const [itemIndex, itemColor] of pattern.mask.entries()) {
14
+ for (let partOffset = itemIndex * itemLength; partOffset < (itemIndex + 1) * itemLength; ++partOffset) {
15
+ const pixelColor = pixelColorAt(image, index + partOffset)
16
+ if (pixelColor !== itemColor) return false
40
17
  }
41
18
  }
42
19
  return true
43
20
  }
44
21
 
45
- function pixelColorAt(image, index, threshold = 0) {
22
+ function pixelColorAt(image, index) {
46
23
  const channels = 4
47
24
  const r = image.data[index * channels]
48
25
  const g = image.data[index * channels + 1]
49
26
  const b = image.data[index * channels + 2]
50
- const rgb = [r, g, b]
51
27
 
52
- // WHITE
53
- if (rgb.every(sub => sub >= 255 - threshold)) return 1
54
- // BLACK
55
- else if (rgb.every(sub => sub <= threshold)) return 0
56
- // OTHER
57
- else return -1
28
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
29
+
30
+ return luminance < 128 ? /* black */ 1 : /* white */ 0
58
31
  }
59
32
 
60
33
  module.exports = findImagePattern
package/src/image.js CHANGED
@@ -6,7 +6,7 @@ const utils = require('@applitools/utils')
6
6
 
7
7
  function makeImage(data) {
8
8
  let image, size
9
- let transforms = {rotate: 0, scale: 1, crop: null}
9
+ let transforms = {rotate: 0, scale: 1, crop: null, modifiers: []}
10
10
 
11
11
  if (utils.types.isBase64(data)) {
12
12
  const buffer = Buffer.from(data, 'base64')
@@ -20,39 +20,45 @@ function makeImage(data) {
20
20
  image = fromBuffer(data)
21
21
  size = extractPngSize(data)
22
22
  } else if (data.isImage) {
23
- image = data.toRaw()
24
- size = data.size
25
23
  transforms = data.transforms
24
+ image = data.toRaw()
25
+ size = utils.geometry.scale(data.size, 1 / transforms.scale)
26
26
  } else if (utils.types.has(data, ['width', 'height'])) {
27
27
  image = fromSize(data)
28
28
  if (data.data) image.data = data.data
29
29
  size = {width: data.width, height: data.height}
30
+ } else if (data.auto) {
31
+ size = {width: -1, height: -1}
30
32
  } else {
31
33
  throw new Error('Unable to create an image abstraction from unknown data')
32
34
  }
33
35
 
34
- if (!transforms.crop) {
35
- transforms.crop = utils.geometry.region({x: 0, y: 0}, size)
36
- }
37
-
38
36
  return {
39
37
  get isImage() {
40
38
  return true
41
39
  },
42
40
  get size() {
43
- return utils.geometry.round(utils.geometry.scale(size, transforms.scale))
41
+ return utils.geometry.round(
42
+ utils.geometry.rotate(utils.geometry.scale(size, transforms.scale), transforms.rotate),
43
+ )
44
44
  },
45
45
  get transforms() {
46
46
  return {...transforms}
47
47
  },
48
48
  get width() {
49
- return size.width
49
+ return this.size.width
50
50
  },
51
51
  get height() {
52
- return size.height
52
+ return this.size.height
53
53
  },
54
54
  scale(ratio) {
55
55
  transforms.scale *= ratio
56
+ // size = utils.geometry.scale(size, ratio)
57
+ return this
58
+ },
59
+ rotate(degrees) {
60
+ transforms.rotate = (transforms.rotate + degrees) % 360
61
+ // size = utils.geometry.rotate(size, degrees)
56
62
  return this
57
63
  },
58
64
  crop(region) {
@@ -66,28 +72,50 @@ function makeImage(data) {
66
72
  } else {
67
73
  region = utils.geometry.scale(region, 1 / transforms.scale)
68
74
  }
69
- region = utils.geometry.rotate(region, transforms.rotate)
70
- transforms.crop = utils.geometry.intersect(transforms.crop, utils.geometry.offset(region, transforms.crop))
71
-
72
- size = utils.geometry.round(utils.geometry.size(transforms.crop))
73
-
75
+ region = utils.geometry.rotate(region, -transforms.rotate, utils.geometry.rotate(size, transforms.rotate))
76
+ region = transforms.crop
77
+ ? utils.geometry.intersect(transforms.crop, utils.geometry.offset(region, transforms.crop))
78
+ : utils.geometry.intersect({x: 0, y: 0, ...size}, region)
79
+ transforms.crop = region
80
+ size = utils.geometry.size(transforms.crop)
74
81
  return this
75
82
  },
76
- rotate(degree) {
77
- transforms.rotate = (transforms.rotate + degree) % 360
83
+ copy(srcImage, offset) {
84
+ const scale = srcImage.transforms.scale
85
+ if (!image) {
86
+ size = {
87
+ width: Math.max(Math.floor((offset.x + srcImage.width) / scale), size.width),
88
+ height: Math.max(Math.floor((offset.y + srcImage.height) / scale), size.height),
89
+ }
90
+ transforms.scale = Math.min(scale, transforms.scale)
91
+ }
92
+ transforms.modifiers.push({
93
+ type: 'copy',
94
+ image: srcImage.scale(scale === transforms.scale ? 1 / scale : scale / transforms.scale).toObject(),
95
+ offset: utils.geometry.scale(offset, 1 / transforms.scale),
96
+ })
97
+
78
98
  return this
79
99
  },
80
- async copy(srcImage, offset) {
81
- const [dst, src] = await Promise.all([this.toObject(), srcImage.toObject()])
82
- image = await copy(dst, src, offset)
100
+ frame(topImage, bottomImage, region) {
101
+ const scale = topImage.transforms.scale
102
+ const prevSize = size
103
+ region = utils.geometry.scale(region, 1 / scale)
104
+ size = {
105
+ width: Math.floor(topImage.width / scale + Math.max(size.width - region.width, 0)),
106
+ height: Math.floor(topImage.height / scale + Math.max(size.height - region.height, 0)),
107
+ }
108
+ transforms.modifiers.push({
109
+ type: 'frame',
110
+ top: topImage.scale(scale === transforms.scale ? 1 / scale : scale / transforms.scale).toObject(),
111
+ bottom: bottomImage.scale(scale === transforms.scale ? 1 / scale : scale / transforms.scale).toObject(),
112
+ region,
113
+ })
114
+ transforms.added = {width: size.width - prevSize.width, height: size.height - prevSize.height}
83
115
  return this
84
116
  },
85
- async combine(firstImage, lastImage, region) {
86
- const [first, last, src] = await Promise.all([firstImage.toObject(), lastImage.toObject(), this.toObject()])
87
- image = await combine(first, last, src, region)
88
- size = {width: image.width, height: image.height}
89
- transforms.crop = utils.geometry.region({x: 0, y: 0}, size)
90
- return this
117
+ async toRaw() {
118
+ return image
91
119
  },
92
120
  async toBuffer() {
93
121
  const image = await this.toObject()
@@ -99,19 +127,17 @@ function makeImage(data) {
99
127
  async toFile(path) {
100
128
  return toFile(await image, path)
101
129
  },
102
- async toRaw() {
103
- return image
104
- },
105
130
  async toObject() {
106
- image = await transform(await image, transforms)
107
- transforms = {rotate: 0, scale: 1, crop: utils.geometry.region({x: 0, y: 0}, size)}
131
+ image = await transform(image ? await image : size, transforms)
132
+ transforms = {crop: null, scale: 1, rotate: 0, modifiers: []}
108
133
  return image
109
134
  },
110
135
  async debug(debug) {
111
136
  if (!debug || !debug.path) return
112
137
  const timestamp = new Date().toISOString().replace(/[-T:.]/g, '_')
113
138
  const filename = ['screenshot', timestamp, debug.name, debug.suffix].filter(part => part).join('_') + '.png'
114
- return toFile(await transform(await image, transforms), path.join(debug.path, filename)).catch(() => null)
139
+ const transformedImage = await transform(image ? await image : size, transforms)
140
+ return toFile(transformedImage, path.join(debug.path, filename)).catch(() => null)
115
141
  },
116
142
  }
117
143
  }
@@ -164,10 +190,27 @@ async function toFile(image, path) {
164
190
  }
165
191
 
166
192
  async function transform(image, transforms) {
167
- const croppedImage = transforms.crop ? await extract(image, transforms.crop) : image
168
- const scaledImage = transforms.scale !== 1 ? await scale(croppedImage, transforms.scale) : croppedImage
169
- const rotatedImage = transforms.rotate > 0 ? await rotate(scaledImage, transforms.rotate) : scaledImage
170
- return rotatedImage
193
+ if (!image.data) {
194
+ const size = transforms.added
195
+ ? {width: image.width - transforms.added.width, height: image.height - transforms.added.height}
196
+ : image
197
+ image = new png.Image(size)
198
+ }
199
+
200
+ image = await transforms.modifiers.reduce(async (image, modifier) => {
201
+ if (modifier.type === 'copy') {
202
+ return copy(await image, await modifier.image, modifier.offset)
203
+ } else if (modifier.type === 'frame') {
204
+ return frame(await modifier.top, await modifier.bottom, await image, modifier.region)
205
+ } else {
206
+ return image
207
+ }
208
+ }, image)
209
+
210
+ image = transforms.crop ? await extract(image, transforms.crop) : image
211
+ image = transforms.scale !== 1 ? await scale(image, transforms.scale) : image
212
+ image = transforms.rotate !== 0 ? await rotate(image, transforms.rotate) : image
213
+ return image
171
214
  }
172
215
 
173
216
  async function scale(image, scaleRatio) {
@@ -256,7 +299,7 @@ async function rotate(image, degrees) {
256
299
  for (let srcY = 0, dstX = 0; srcY < image.height; ++srcY, ++dstX) {
257
300
  for (let srcX = 0, dstY = image.width - 1; srcX < image.width; ++srcX, --dstY) {
258
301
  const pixel = image.data.readUInt32BE((srcY * image.width + srcX) * 4)
259
- dstImage.data.writeUInt32BE(pixel, (srcX * dstImage.width + dstY) * 4)
302
+ dstImage.data.writeUInt32BE(pixel, (dstY * dstImage.width + dstX) * 4)
260
303
  }
261
304
  }
262
305
  } else {
@@ -288,98 +331,98 @@ async function copy(dstImage, srcImage, offset) {
288
331
  return dstImage
289
332
  }
290
333
 
291
- async function combine(firstImage, lastImage, srcImage, region) {
334
+ async function frame(topImage, bottomImage, srcImage, region) {
292
335
  region = utils.geometry.intersect(
293
- {x: 0, y: 0, width: firstImage.width, height: firstImage.height},
336
+ {x: 0, y: 0, width: topImage.width, height: topImage.height},
294
337
  utils.geometry.round(region),
295
338
  )
296
339
 
297
- if (region.x === 0 && region.y === 0 && region.width >= firstImage.width && region.height >= firstImage.height) {
340
+ if (region.x === 0 && region.y === 0 && region.width >= topImage.width && region.height >= topImage.height) {
298
341
  return srcImage
299
342
  }
300
343
 
301
- if (region.width === srcImage.width && region.height === srcImage.height) {
302
- await copy(firstImage, srcImage, {x: region.x, y: region.y})
303
- return firstImage
344
+ if (region.width >= srcImage.width && region.height >= srcImage.height) {
345
+ await copy(topImage, srcImage, {x: region.x, y: region.y})
346
+ return topImage
304
347
  }
305
348
 
306
349
  const dstImage = new png.Image({
307
- width: firstImage.width - region.width + srcImage.width,
308
- height: firstImage.height - region.height + srcImage.height,
350
+ width: topImage.width + Math.max(srcImage.width - region.width, 0),
351
+ height: topImage.height + Math.max(srcImage.height - region.height, 0),
309
352
  })
310
353
 
311
354
  if (region.width === srcImage.width) {
312
- const topImage = await extract(firstImage, {
355
+ const topExtImage = await extract(topImage, {
313
356
  x: 0,
314
357
  y: 0,
315
- width: firstImage.width,
358
+ width: topImage.width,
316
359
  height: region.y + region.height,
317
360
  })
318
- await copy(dstImage, topImage, {x: 0, y: 0})
361
+ await copy(dstImage, topExtImage, {x: 0, y: 0})
319
362
  } else if (region.height === srcImage.height) {
320
- const leftImage = await extract(firstImage, {
363
+ const leftExtImage = await extract(topImage, {
321
364
  x: 0,
322
365
  y: 0,
323
366
  width: region.x + region.width,
324
- height: firstImage.height,
367
+ height: topImage.height,
325
368
  })
326
- await copy(dstImage, leftImage, {x: 0, y: 0})
369
+ await copy(dstImage, leftExtImage, {x: 0, y: 0})
327
370
  } else {
328
- const topLeftImage = await extract(firstImage, {
371
+ const topLeftExtImage = await extract(topImage, {
329
372
  x: 0,
330
373
  y: 0,
331
374
  width: region.x + region.width,
332
375
  height: region.y + region.height,
333
376
  })
334
- await copy(dstImage, topLeftImage, {x: 0, y: 0})
377
+ await copy(dstImage, topLeftExtImage, {x: 0, y: 0})
335
378
 
336
- const rightExtImage = await extract(firstImage, {
379
+ const rightExtImage = await extract(topImage, {
337
380
  x: region.x + region.width,
338
381
  y: 0,
339
- width: firstImage.width - (region.x + region.width),
382
+ width: topImage.width - (region.x + region.width),
340
383
  height: region.y,
341
384
  })
342
385
  await copy(dstImage, rightExtImage, {x: region.x + region.width, y: 0})
343
386
 
344
- const bottomExtImage = await extract(firstImage, {
387
+ const bottomExtImage = await extract(topImage, {
345
388
  x: 0,
346
389
  y: region.y + region.height,
347
390
  width: region.x,
348
- height: firstImage.height - (region.y + region.height),
391
+ height: topImage.height - (region.y + region.height),
349
392
  })
350
393
  await copy(dstImage, bottomExtImage, {x: 0, y: region.y + region.height})
351
394
  }
352
395
 
353
- if (lastImage.height > region.y + region.height || lastImage.width > region.x + region.width) {
396
+ if (bottomImage.height > region.y + region.height || bottomImage.width > region.x + region.width) {
354
397
  // first image might be higher
355
- const yDiff = firstImage.height - lastImage.height
398
+ const yDiff = topImage.height - bottomImage.height
356
399
  if (region.width === srcImage.width) {
357
- const bottomImage = await extract(lastImage, {
400
+ const bottomExtImage = await extract(bottomImage, {
358
401
  x: 0,
359
402
  y: region.y - yDiff + region.height,
360
- width: lastImage.width,
361
- height: lastImage.height - (region.y - yDiff + region.height),
403
+ width: bottomImage.width,
404
+ height: bottomImage.height - (region.y - yDiff + region.height),
362
405
  })
363
- await copy(dstImage, bottomImage, {x: 0, y: region.y + srcImage.height})
406
+ await copy(dstImage, bottomExtImage, {x: 0, y: region.y + Math.max(srcImage.height, region.height)})
364
407
  } else if (region.height === srcImage.height) {
365
- const rightImage = await extract(lastImage, {
408
+ const rightExtImage = await extract(bottomImage, {
366
409
  x: region.x + region.width,
367
410
  y: 0,
368
- width: lastImage.width - (region.x + region.width),
369
- height: lastImage.height,
411
+ width: bottomImage.width - (region.x + region.width),
412
+ height: bottomImage.height,
370
413
  })
371
- await copy(dstImage, rightImage, {x: region.x + srcImage.width, y: 0})
414
+ await copy(dstImage, rightExtImage, {x: region.x + Math.max(srcImage.width, region.width), y: 0})
372
415
  } else {
373
- const bottomRightImage = await extract(lastImage, {
416
+ const bottomRightExtImage = await extract(bottomImage, {
374
417
  x: region.x,
375
418
  y: region.y - yDiff,
376
- width: lastImage.width - region.x,
377
- height: lastImage.height - (region.y - yDiff),
419
+ width: bottomImage.width - region.x,
420
+ height: bottomImage.height - (region.y - yDiff),
378
421
  })
379
422
 
380
- await copy(dstImage, bottomRightImage, {
381
- x: region.x + srcImage.width - region.width,
382
- y: region.y + srcImage.height - region.height,
423
+ await copy(dstImage, bottomRightExtImage, {
424
+ x: region.x + Math.max(srcImage.width - region.width, 0),
425
+ y: region.y + Math.max(srcImage.height - region.height, 0),
383
426
  })
384
427
  }
385
428
  }
@@ -15,14 +15,23 @@ async function scrollIntoViewport({context, scroller, region, logger}) {
15
15
  let remainingOffset = {x: elementContextRegion.x, y: elementContextRegion.y}
16
16
  while (currentContext) {
17
17
  const scrollingElement = await currentContext.getScrollingElement()
18
- const scrollingElementOffset = scrollingElement
19
- ? utils.geometry.location(await scrollingElement.getClientRegion())
20
- : {x: 0, y: 0}
18
+ if (!scrollingElement) {
19
+ currentContext = currentContext.parent
20
+ continue
21
+ }
21
22
 
22
- const actualOffset = await scroller.moveTo(
23
- utils.geometry.offsetNegative(remainingOffset, scrollingElementOffset),
24
- scrollingElement,
25
- )
23
+ const scrollableRegion = await scrollingElement.getClientRegion()
24
+ const requiredOffset = {
25
+ x: Math.max(
26
+ remainingOffset.x - (scrollableRegion.x + Math.max(scrollableRegion.width - elementContextRegion.width, 0)),
27
+ 0,
28
+ ),
29
+ y: Math.max(
30
+ remainingOffset.y - (scrollableRegion.y + Math.max(scrollableRegion.height - elementContextRegion.height, 0)),
31
+ 0,
32
+ ),
33
+ }
34
+ const actualOffset = await scroller.moveTo(requiredOffset, scrollingElement)
26
35
 
27
36
  remainingOffset = utils.geometry.offset(
28
37
  utils.geometry.offsetNegative(remainingOffset, actualOffset),