@applitools/screenshoter 3.3.6 → 3.3.7

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@
4
4
  ## Unreleased
5
5
 
6
6
 
7
+ ## 3.3.7 - 2022/2/15
8
+
9
+ - fix image scaling on pages without viewport metatag
10
+ - fix safari's viewport detection on iOS devices
11
+ - updated to @applitools/snippets@2.1.13 (from 2.1.12)
12
+ - updated to @applitools/utils@1.2.13 (from 1.2.12)
13
+
7
14
  ## 3.3.6 - 2022/2/9
8
15
 
9
16
  - fix testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/screenshoter",
3
- "version": "3.3.6",
3
+ "version": "3.3.7",
4
4
  "description": "Applitools universal screenshoter for web and native applications",
5
5
  "keywords": [
6
6
  "applitools",
@@ -43,6 +43,7 @@
43
43
  "setup:android": "yarn android:setup && yarn appium:setup",
44
44
  "setup:ios": "yarn ios:setup && yarn appium:setup",
45
45
  "android:setup": "node ./scripts/android-emulator.js",
46
+ "android:shutdown": "adb -s emulator-5555 emu kill || true && adb -s emulator-5557 emu kill || true",
46
47
  "docker:setup": "node ../scripts/scripts/generate-docker-compose-config.js && docker-compose up -d",
47
48
  "docker:teardown": "docker-compose down",
48
49
  "ios:setup": "node ./scripts/ios-simulator.js",
@@ -59,15 +60,15 @@
59
60
  }
60
61
  },
61
62
  "dependencies": {
62
- "@applitools/snippets": "2.1.12",
63
- "@applitools/utils": "1.2.12",
63
+ "@applitools/snippets": "2.1.13",
64
+ "@applitools/utils": "1.2.13",
64
65
  "png-async": "0.9.4"
65
66
  },
66
67
  "devDependencies": {
67
- "@applitools/driver": "1.4.12",
68
+ "@applitools/driver": "1.4.14",
68
69
  "@applitools/sdk-release-kit": "0.13.11",
69
70
  "@applitools/spec-driver-webdriverio": "1.2.7",
70
- "@applitools/test-utils": "1.0.11",
71
+ "@applitools/test-utils": "1.0.12",
71
72
  "appium": "^1.22.2",
72
73
  "chromedriver": "^95.0.0",
73
74
  "eslint": "^7.9.0",
@@ -81,6 +82,6 @@
81
82
  "webdriverio": "^7.16.7"
82
83
  },
83
84
  "engines": {
84
- "node": ">= 8.9.0"
85
+ "node": ">= 10.0.0"
85
86
  }
86
87
  }
@@ -1,19 +1,22 @@
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
- const patterOffset = pattern.offset * pattern.pixelRatio
4
+ const patterOffset = Math.round(pattern.offset * pattern.scale)
5
5
  return {x: (pixel % image.width) - patterOffset, y: Math.floor(pixel / image.width) - patterOffset}
6
6
  }
7
7
  }
8
8
  return null
9
9
  }
10
10
 
11
- function isPattern(image, index, pattern) {
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
11
+ function isPattern(image, offset, pattern) {
12
+ const length = Math.round(pattern.size * pattern.scale)
13
+ for (const [index, color] of pattern.mask.entries()) {
14
+ const maxLength = index * pattern.size * pattern.scale // how many pixels actually could be occupied at this point
15
+ const missedPixels = Math.abs(maxLength - Math.round(maxLength)) // how many pixels were missed due to rounding
16
+ const skippedPixels = missedPixels >= 0.25 ? Math.ceil(missedPixels) : 0 // how many pixels should be skipped from checking in pattern (usually 1 or 0)
17
+ for (let pixel = index * length; pixel < (index + 1) * length - skippedPixels; ++pixel) {
18
+ const pixelColor = pixelColorAt(image, offset + pixel)
19
+ if (pixelColor !== color) return false
17
20
  }
18
21
  }
19
22
  return true
@@ -27,7 +30,10 @@ function pixelColorAt(image, index) {
27
30
 
28
31
  const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
29
32
 
30
- return luminance < 128 ? /* black */ 1 : /* white */ 0
33
+ // if luminance is between black and white check the color of previous pixel
34
+ if (luminance >= 112 && luminance <= 144) return pixelColorAt(image, index - 1)
35
+ else if (luminance < 128) return /* black */ 1
36
+ else return /* white*/ 0
31
37
  }
32
38
 
33
39
  module.exports = findImagePattern
package/src/image.js CHANGED
@@ -22,7 +22,7 @@ function makeImage(data) {
22
22
  } else if (data.isImage) {
23
23
  transforms = data.transforms
24
24
  image = data.toRaw()
25
- size = utils.geometry.scale(data.size, 1 / transforms.scale)
25
+ size = data.rawSize
26
26
  } else if (utils.types.has(data, ['width', 'height'])) {
27
27
  image = fromSize(data)
28
28
  if (data.data) image.data = data.data
@@ -38,9 +38,18 @@ function makeImage(data) {
38
38
  return true
39
39
  },
40
40
  get size() {
41
- return utils.geometry.round(
42
- utils.geometry.rotate(utils.geometry.scale(size, transforms.scale), transforms.rotate),
43
- )
41
+ const croppedSize = utils.geometry.size(transforms.crop || size)
42
+ const scaledSize = utils.geometry.scale(croppedSize, transforms.scale)
43
+ const rotatedSize = utils.geometry.rotate(scaledSize, transforms.rotate)
44
+ return utils.geometry.round(rotatedSize)
45
+ },
46
+ get unscaledSize() {
47
+ const croppedSize = utils.geometry.size(transforms.crop || size)
48
+ const rotatedSize = utils.geometry.rotate(croppedSize, transforms.rotate)
49
+ return utils.geometry.round(rotatedSize)
50
+ },
51
+ get rawSize() {
52
+ return size
44
53
  },
45
54
  get transforms() {
46
55
  return {...transforms}
@@ -53,12 +62,10 @@ function makeImage(data) {
53
62
  },
54
63
  scale(ratio) {
55
64
  transforms.scale *= ratio
56
- // size = utils.geometry.scale(size, ratio)
57
65
  return this
58
66
  },
59
67
  rotate(degrees) {
60
68
  transforms.rotate = (transforms.rotate + degrees) % 360
61
- // size = utils.geometry.rotate(size, degrees)
62
69
  return this
63
70
  },
64
71
  crop(region) {
@@ -77,39 +84,47 @@ function makeImage(data) {
77
84
  ? utils.geometry.intersect(transforms.crop, utils.geometry.offset(region, transforms.crop))
78
85
  : utils.geometry.intersect({x: 0, y: 0, ...size}, region)
79
86
  transforms.crop = region
80
- size = utils.geometry.size(transforms.crop)
81
87
  return this
82
88
  },
83
89
  copy(srcImage, offset) {
84
- const scale = srcImage.transforms.scale
90
+ // if "auto" image and this is first chunk
91
+ if (!image && size.width === -1 && size.height === -1) transforms.scale = srcImage.transforms.scale
92
+
93
+ const unscaledOffset = utils.geometry.scale(offset, 1 / transforms.scale)
94
+
85
95
  if (!image) {
86
96
  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),
97
+ width: Math.max(Math.floor(unscaledOffset.x) + srcImage.unscaledSize.width, size.width),
98
+ height: Math.max(Math.floor(unscaledOffset.y) + srcImage.unscaledSize.height, size.height),
89
99
  }
90
- transforms.scale = Math.min(scale, transforms.scale)
91
100
  }
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
- })
101
+
102
+ const unscale =
103
+ srcImage.transforms.scale === transforms.scale
104
+ ? 1 / srcImage.transforms.scale
105
+ : srcImage.transforms.scale / transforms.scale
106
+
107
+ transforms.modifiers.push({type: 'copy', image: srcImage.scale(unscale).toObject(), offset: unscaledOffset})
97
108
 
98
109
  return this
99
110
  },
100
111
  frame(topImage, bottomImage, region) {
101
- const scale = topImage.transforms.scale
102
112
  const prevSize = size
103
- region = utils.geometry.scale(region, 1 / scale)
113
+ const unscaledRegion = utils.geometry.scale(region, 1 / topImage.transforms.scale)
104
114
  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)),
115
+ width: Math.floor(topImage.unscaledSize.width + Math.max(size.width - unscaledRegion.width, 0)),
116
+ height: Math.floor(topImage.unscaledSize.height + Math.max(size.height - unscaledRegion.height, 0)),
107
117
  }
118
+
119
+ const unscale =
120
+ topImage.transforms.scale === transforms.scale
121
+ ? 1 / topImage.transforms.scale
122
+ : topImage.transforms.scale / transforms.scale
108
123
  transforms.modifiers.push({
109
124
  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,
125
+ top: topImage.scale(unscale).toObject(),
126
+ bottom: bottomImage.scale(unscale).toObject(),
127
+ region: unscaledRegion,
113
128
  })
114
129
  transforms.added = {width: size.width - prevSize.width, height: size.height - prevSize.height}
115
130
  return this
package/src/scroller.js CHANGED
@@ -80,7 +80,6 @@ function makeScroller({logger, element, scrollingMode = 'mixed'}) {
80
80
 
81
81
  async function scrollTo(offset, element = defaultElement) {
82
82
  try {
83
- // offset = {x: Math.max(offset.x, 0), y: Math.max(offset.y, 0)}
84
83
  const scrollOffset = await element.scrollTo(offset)
85
84
  return scrollOffset
86
85
  } catch (err) {
@@ -92,7 +91,6 @@ function makeScroller({logger, element, scrollingMode = 'mixed'}) {
92
91
 
93
92
  async function translateTo(offset, element = defaultElement) {
94
93
  try {
95
- // offset = {x: Math.max(offset.x, 0), y: Math.max(offset.y, 0)}
96
94
  await element.scrollTo({x: 0, y: 0})
97
95
  const translateOffset = await element.translateTo(offset)
98
96
  return translateOffset
@@ -105,8 +103,15 @@ function makeScroller({logger, element, scrollingMode = 'mixed'}) {
105
103
 
106
104
  async function shiftTo(offset, element = defaultElement) {
107
105
  try {
108
- // offset = {x: Math.max(offset.x, 0), y: Math.max(offset.y, 0)}
109
106
  const scrollOffset = await element.scrollTo(offset)
107
+ if (utils.geometry.equals(scrollOffset, offset)) return scrollOffset
108
+
109
+ // there is a "bug" in iOS that will not move a root element if it already scrolled, so it should be translated all the way
110
+ if (element.driver.isIOS && (await element.isRoot())) {
111
+ const translateOffset = await element.translateTo(offset)
112
+ return translateOffset
113
+ }
114
+
110
115
  const remainingOffset = utils.geometry.offsetNegative(offset, scrollOffset)
111
116
  const translateOffset = await element.translateTo(remainingOffset)
112
117
 
@@ -60,6 +60,8 @@ async function takeScreenshot({
60
60
  ? await takeStitchedScreenshot({...target, withStatusBar, overlap, framed, wait, stabilization, debug, logger})
61
61
  : await takeSimpleScreenshot({...target, withStatusBar, wait, stabilization, debug, logger})
62
62
 
63
+ screenshot.image.scale(driver.viewportScale)
64
+
63
65
  if (hooks && hooks.afterScreenshot) {
64
66
  // imitate image-like state for the hook
65
67
  if (window && fully && target.scroller) {
@@ -33,7 +33,7 @@ function makeTakeDefaultScreenshot({driver, stabilization = {}, debug, logger})
33
33
  await image.debug({...debug, name, suffix: 'original'})
34
34
 
35
35
  if (stabilization.scale) image.scale(stabilization.scale)
36
- else image.scale(1 / driver.pixelRatio)
36
+ else image.scale(1 / driver.pixelRatio / driver.viewportScale)
37
37
 
38
38
  if (stabilization.rotate) image.crop(stabilization.rotate)
39
39
 
@@ -53,7 +53,7 @@ function makeTakeMainContextScreenshot({driver, stabilization = {}, debug, logge
53
53
  await image.debug({...debug, name, suffix: 'original'})
54
54
 
55
55
  if (stabilization.scale) image.scale(stabilization.scale)
56
- else image.scale(1 / driver.pixelRatio)
56
+ else image.scale(1 / driver.pixelRatio / driver.viewportScale)
57
57
 
58
58
  if (stabilization.rotate) image.rotate(stabilization.rotate)
59
59
 
@@ -72,7 +72,7 @@ function makeTakeSafari11Screenshot({driver, stabilization = {}, debug, logger})
72
72
  await image.debug({...debug, name, suffix: 'original'})
73
73
 
74
74
  if (stabilization.scale) image.scale(stabilization.scale)
75
- else image.scale(1 / driver.pixelRatio)
75
+ else image.scale(1 / driver.pixelRatio / driver.viewportScale)
76
76
 
77
77
  if (stabilization.rotate) image.rotate(stabilization.rotate)
78
78
 
@@ -96,7 +96,7 @@ function makeTakeMarkedScreenshot({driver, stabilization = {}, debug, logger}) {
96
96
  await image.debug({...debug, name, suffix: 'original'})
97
97
 
98
98
  if (stabilization.scale) image.scale(stabilization.scale)
99
- else image.scale(1 / driver.pixelRatio)
99
+ else image.scale(1 / driver.pixelRatio / driver.viewportScale)
100
100
 
101
101
  if (stabilization.rotate) image.rotate(stabilization.rotate)
102
102
  else if (driver.orientation === 'landscape' && image.width < image.height) image.rotate(-90)
@@ -113,7 +113,13 @@ function makeTakeMarkedScreenshot({driver, stabilization = {}, debug, logger}) {
113
113
  }
114
114
 
115
115
  async function getViewportRegion() {
116
- const marker = await driver.mainContext.execute(snippets.addPageMarker)
116
+ // marker is -> bwb bwbb wbw bwbb wbww bbb
117
+ const marker = await driver.mainContext.execute(snippets.addPageMarker, [
118
+ {
119
+ mask: [1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1],
120
+ size: utils.math.multiplier(driver.viewportScale * driver.pixelRatio, 0.05),
121
+ },
122
+ ])
117
123
  await utils.general.sleep(100)
118
124
 
119
125
  try {
@@ -124,12 +130,16 @@ function makeTakeMarkedScreenshot({driver, stabilization = {}, debug, logger}) {
124
130
 
125
131
  await image.debug({...debug, name: 'marker'})
126
132
 
127
- const markerLocation = findImagePattern(await image.toObject(), {...marker, pixelRatio: driver.pixelRatio})
133
+ const markerLocation = findImagePattern(await image.toObject(), {
134
+ ...marker,
135
+ scale: driver.viewportScale * driver.pixelRatio,
136
+ })
128
137
  if (!markerLocation) return null
129
138
 
130
- const viewportSize = await driver.getViewportSize()
131
-
132
- return utils.geometry.region(utils.geometry.scale(markerLocation, 1 / driver.pixelRatio), viewportSize)
139
+ return utils.geometry.region(
140
+ utils.geometry.scale(markerLocation, 1 / driver.pixelRatio / driver.viewportScale),
141
+ await driver.getViewportSize(),
142
+ )
133
143
  } finally {
134
144
  await driver.mainContext.execute(snippets.cleanupPageMarker)
135
145
  }
@@ -143,7 +153,7 @@ function makeTakeNativeScreenshot({driver, stabilization = {}, debug, logger}) {
143
153
  await image.debug({...debug, name, suffix: 'original'})
144
154
 
145
155
  if (stabilization.scale) image.scale(stabilization.scale)
146
- else image.scale(1 / driver.pixelRatio)
156
+ else image.scale(1 / driver.pixelRatio / driver.viewportScale)
147
157
 
148
158
  if (stabilization.rotate) image.rotate(stabilization.rotate)
149
159
  else if (driver.orientation === 'landscape' && image.width < image.height) image.rotate(-90)