@@ -241,7 +241,7 @@ jobs:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 5 --index 0
|
||||
command: npm run test-e2e -- --parallel 7 --index 0
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 5 --index 1
|
||||
command: npm run test-e2e -- --parallel 7 --index 1
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -263,7 +263,7 @@ jobs:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 5 --index 2
|
||||
command: npm run test-e2e -- --parallel 7 --index 2
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 5 --index 3
|
||||
command: npm run test-e2e -- --parallel 7 --index 3
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -285,7 +285,29 @@ jobs:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 5 --index 4
|
||||
command: npm run test-e2e -- --parallel 7 --index 4
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
|
||||
"server-e2e-tests-6":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 7 --index 5
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
|
||||
"server-e2e-tests-7":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm run test-e2e -- --parallel 7 --index 6
|
||||
working_directory: packages/server
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -303,7 +325,7 @@ jobs:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 5 --index 0
|
||||
command: npm run cypress:run -- --parallel 7 --index 0
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -323,7 +345,7 @@ jobs:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 5 --index 1
|
||||
command: npm run cypress:run -- --parallel 7 --index 1
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -343,7 +365,7 @@ jobs:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 5 --index 2
|
||||
command: npm run cypress:run -- --parallel 7 --index 2
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -363,7 +385,7 @@ jobs:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 5 --index 3
|
||||
command: npm run cypress:run -- --parallel 7 --index 3
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -383,7 +405,47 @@ jobs:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 5 --index 4
|
||||
command: npm run cypress:run -- --parallel 7 --index 4
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
|
||||
"driver-integration-tests-6":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm start
|
||||
working_directory: packages/driver
|
||||
background: true
|
||||
- run:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 7 --index 5
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
|
||||
"driver-integration-tests-7":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: cypress-{{ .Branch }}-{{ .Revision }}
|
||||
- run:
|
||||
command: npm start
|
||||
working_directory: packages/driver
|
||||
background: true
|
||||
- run:
|
||||
command: $(npm bin)/wait-on http://localhost:3500
|
||||
working_directory: packages/driver
|
||||
- run:
|
||||
command: npm run cypress:run -- --parallel 7 --index 6
|
||||
working_directory: packages/driver
|
||||
- store_test_results:
|
||||
path: /tmp/cypress
|
||||
@@ -681,6 +743,12 @@ workflows:
|
||||
- server-e2e-tests-5:
|
||||
requires:
|
||||
- build
|
||||
- server-e2e-tests-6:
|
||||
requires:
|
||||
- build
|
||||
- server-e2e-tests-7:
|
||||
requires:
|
||||
- build
|
||||
- driver-integration-tests-1:
|
||||
requires:
|
||||
- build
|
||||
@@ -696,6 +764,12 @@ workflows:
|
||||
- driver-integration-tests-5:
|
||||
requires:
|
||||
- build
|
||||
- driver-integration-tests-6:
|
||||
requires:
|
||||
- build
|
||||
- driver-integration-tests-7:
|
||||
requires:
|
||||
- build
|
||||
- desktop-gui-integration-tests-1:
|
||||
requires:
|
||||
- build
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"bluebird": "3.5.0",
|
||||
"body-parser": "^1.12.4",
|
||||
"bootstrap": "^3.3.5",
|
||||
"bytes": "^3.0.0",
|
||||
"card": "^1.0.2",
|
||||
"chai": "3.5.0",
|
||||
"chai-as-promised": "6.0.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
_ = require("lodash")
|
||||
Promise = require("bluebird")
|
||||
$ = require("jquery")
|
||||
bytes = require("bytes")
|
||||
Promise = require("bluebird")
|
||||
|
||||
$Screenshot = require("../../cypress/screenshot")
|
||||
$dom = require("../../dom")
|
||||
@@ -283,8 +284,9 @@ module.exports = (Commands, Cypress, cy, state, config) ->
|
||||
## failure screenshot when not interactive
|
||||
Cypress.on "runnable:after:run:async", (test, runnable) ->
|
||||
screenshotConfig = $Screenshot.getConfig()
|
||||
|
||||
return if not test.err or not screenshotConfig.screenshotOnRunFailure or config("isInteractive")
|
||||
|
||||
|
||||
if not state("screenshotTaken")
|
||||
## if a screenshot has not been taken (by cy.screenshot()) in the
|
||||
## test that failed, we can bypass UI-changing and pixel-checking
|
||||
@@ -364,13 +366,14 @@ module.exports = (Commands, Cypress, cy, state, config) ->
|
||||
timeout: options.timeout
|
||||
})
|
||||
.then (props) ->
|
||||
{ duration, path } = props
|
||||
{ duration, path, size } = props
|
||||
{ width, height } = props.dimensions
|
||||
|
||||
takenPaths = state("screenshotPaths") or []
|
||||
state("screenshotPaths", takenPaths.concat([path]))
|
||||
|
||||
_.extend(consoleProps, props, {
|
||||
size: bytes(size, { unitSeparator: " " })
|
||||
duration: "#{duration}ms"
|
||||
dimensions: "#{width}px x #{height}px"
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ describe "src/cy/commands/screenshot", ->
|
||||
|
||||
@serverResult = {
|
||||
path: "/path/to/screenshot"
|
||||
size: "12 B"
|
||||
size: 12
|
||||
dimensions: { width: 20, height: 20 }
|
||||
multipart: false
|
||||
pixelRatio: 1
|
||||
@@ -741,13 +741,14 @@ describe "src/cy/commands/screenshot", ->
|
||||
scaled: true
|
||||
})
|
||||
|
||||
expected = _.omit(expected, "blackout", "dimensions", "screenshotOnRunFailure", "scale")
|
||||
expected = _.omit(expected, "blackout", "dimensions", "screenshotOnRunFailure", "scale", "size")
|
||||
|
||||
cy.screenshot().then =>
|
||||
consoleProps = @lastLog.invoke("consoleProps")
|
||||
actual = _.omit(consoleProps, "blackout", "dimensions", "duration")
|
||||
actual = _.omit(consoleProps, "blackout", "dimensions", "duration", "size")
|
||||
{ width, height } = @serverResult.dimensions
|
||||
expect(actual).to.eql(expected)
|
||||
expect(consoleProps.size).to.eq("12 B")
|
||||
expect(consoleProps.blackout).to.eql(@screenshotConfig.blackout)
|
||||
expect(consoleProps.dimensions).to.eql("#{width}px x #{height}px")
|
||||
expect(consoleProps.duration).to.match(/^\d+ms$/)
|
||||
|
||||
@@ -20,6 +20,7 @@ exports['e2e screenshots passes 1'] = `
|
||||
✓ manually generates pngs
|
||||
✓ can nest screenshots in folders
|
||||
1) generates pngs on failure
|
||||
✓ does not call onAfterScreenshot with results of failed tests
|
||||
✓ handles devicePixelRatio correctly on headless electron
|
||||
✓ crops app captures to just app size
|
||||
✓ can capture fullPage screenshots
|
||||
@@ -41,7 +42,7 @@ exports['e2e screenshots passes 1'] = `
|
||||
5) "after each" hook for "empty test 2"
|
||||
|
||||
|
||||
14 passing
|
||||
15 passing
|
||||
5 failing
|
||||
|
||||
1) taking screenshots generates pngs on failure:
|
||||
@@ -76,8 +77,8 @@ Because this error occurred during a 'after each' hook we are skipping the remai
|
||||
(Results)
|
||||
|
||||
┌───────────────────────────────────────┐
|
||||
│ Tests: 18 │
|
||||
│ Passing: 14 │
|
||||
│ Tests: 19 │
|
||||
│ Passing: 15 │
|
||||
│ Failing: 4 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
@@ -100,7 +101,7 @@ Because this error occurred during a 'after each' hook we are skipping the remai
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/fullPage-same.png (600x500)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/pathological.png (1280x720)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/element.png (400x300)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/taking screenshots -- retries each screenshot for up to XX:XX.png (400x1316)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/taking screenshots -- retries each screenshot for up to XX:XX.png (200x1300)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/taking screenshots -- ensures unique paths for non-named screenshots.png (1280x720)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/taking screenshots -- ensures unique paths for non-named screenshots (1).png (1280x720)
|
||||
- /foo/bar/.projects/e2e/cypress/screenshots/screenshots_spec.coffee/taking screenshots -- ensures unique paths for non-named screenshots (2).png (1280x720)
|
||||
@@ -128,9 +129,9 @@ Because this error occurred during a 'after each' hook we are skipping the remai
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✖ screenshots_spec.coffee XX:XX 18 14 4 - - │
|
||||
│ ✖ screenshots_spec.coffee XX:XX 19 15 4 - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
1 of 1 failed (100%) XX:XX 18 14 4 - -
|
||||
1 of 1 failed (100%) XX:XX 19 15 4 - -
|
||||
|
||||
`
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
_ = require("lodash")
|
||||
mime = require("mime")
|
||||
path = require("path")
|
||||
bytes = require("bytes")
|
||||
Promise = require("bluebird")
|
||||
dataUriToBuffer = require("data-uri-to-buffer")
|
||||
Jimp = require("jimp")
|
||||
@@ -24,8 +23,6 @@ __ID__ = null
|
||||
## screenshots since its possible two screenshots with
|
||||
## the same name will be written to the file system
|
||||
|
||||
Jimp.prototype.getBuffer = Promise.promisify(Jimp.prototype.getBuffer)
|
||||
|
||||
replaceInvalidChars = (str) ->
|
||||
str.replace(invalidCharsRe, "")
|
||||
|
||||
@@ -83,7 +80,7 @@ hasHelperPixels = (image, pixelRatio) ->
|
||||
topRight.isWhite = isWhite(topRight)
|
||||
bottomRight.isBlack = isBlack(bottomRight)
|
||||
|
||||
debug("helper pixels %O", {
|
||||
debug("helper pixels \n %O", {
|
||||
topLeft
|
||||
topLeftRight
|
||||
topLeftDown
|
||||
@@ -113,7 +110,7 @@ captureAndCheck = (data, automate, conditionFn) ->
|
||||
|
||||
automate(data)
|
||||
.then (dataUrl) ->
|
||||
debug("received screenshot data from automation layer")
|
||||
debug("received screenshot data from automation layer", dataUrl.slice(0, 100))
|
||||
|
||||
Jimp.read(dataUriToBuffer(dataUrl))
|
||||
.then (image) ->
|
||||
@@ -133,18 +130,21 @@ isMultipart = (data) ->
|
||||
|
||||
crop = (image, dimensions, pixelRatio = 1) ->
|
||||
debug("dimensions before are %o", dimensions)
|
||||
|
||||
dimensions = _.transform dimensions, (result, value, dimension) ->
|
||||
result[dimension] = value * pixelRatio
|
||||
|
||||
debug("dimensions for cropping are %o", dimensions)
|
||||
|
||||
x = Math.min(dimensions.x, image.bitmap.width - 1)
|
||||
y = Math.min(dimensions.y, image.bitmap.height - 1)
|
||||
width = Math.min(dimensions.width, image.bitmap.width - x)
|
||||
height = Math.min(dimensions.height, image.bitmap.height - y)
|
||||
debug("crop: from #{image.bitmap.width} x #{image.bitmap.height}")
|
||||
debug(" to #{width} x #{height} at (#{x}, #{y})")
|
||||
|
||||
image.crop(x, y, width, height)
|
||||
debug("crop: from #{x}, #{y}")
|
||||
debug(" to #{width} x #{height}")
|
||||
|
||||
image.clone().crop(x, y, width, height)
|
||||
|
||||
pixelConditionFn = (data, image) ->
|
||||
pixelRatio = image.bitmap.width / data.viewport.width
|
||||
@@ -152,57 +152,118 @@ pixelConditionFn = (data, image) ->
|
||||
hasPixels = hasHelperPixels(image, pixelRatio)
|
||||
app = isAppOnly(data)
|
||||
|
||||
subject = if app then "app" else "runner"
|
||||
|
||||
## if we are app, we dont need helper pixels else we do!
|
||||
passes = if app then not hasPixels else hasPixels
|
||||
|
||||
debug("pixelConditionFn", { pixelRatio, hasPixels, app, passes })
|
||||
debug("pixelConditionFn %o", {
|
||||
pixelRatio,
|
||||
subject,
|
||||
hasPixels,
|
||||
expectedPixels: !app
|
||||
})
|
||||
|
||||
return passes
|
||||
|
||||
multipartImages = []
|
||||
|
||||
# compareUntilPixelsDiffer = (img1, img2) ->
|
||||
## NOTE: this is for comparing pixel by pixel which is useful
|
||||
## if you're trying to dig into the specific pixel differences
|
||||
##
|
||||
## we're making this as efficient as possible because
|
||||
## there are significant performance problems
|
||||
## getting a hash or buffer of all the image data.
|
||||
##
|
||||
## instead we will walk through two images comparing
|
||||
## them pixel by pixel until they don't match.
|
||||
#
|
||||
# iterations = 0
|
||||
#
|
||||
# { width, height } = img2.bitmap
|
||||
#
|
||||
# data1 = img1.bitmap.data
|
||||
# data2 = img2.bitmap.data
|
||||
#
|
||||
# ret = (differences) ->
|
||||
# return {
|
||||
# iterations
|
||||
# differences
|
||||
# }
|
||||
#
|
||||
# for y in [0...height]
|
||||
# for x in [0...width]
|
||||
# iterations += 1
|
||||
#
|
||||
# idx = (width * y + x) << 2
|
||||
#
|
||||
# pix1 = data1.readUInt32BE(idx)
|
||||
# pix2 = data2.readUInt32BE(idx)
|
||||
#
|
||||
# if pix1 isnt pix2
|
||||
# return ret([
|
||||
# intToRGBA(pix1),
|
||||
# intToRGBA(pix2)
|
||||
# ])
|
||||
#
|
||||
# return ret(null)
|
||||
|
||||
clearMultipartState = ->
|
||||
debug("clearing %d cached multipart images", multipartImages.length)
|
||||
multipartImages = []
|
||||
|
||||
compareLast = (data, image) ->
|
||||
## ensure the previous image isn't the same, which might indicate the
|
||||
## page has not scrolled yet
|
||||
imagesMatch = (img1, img2) ->
|
||||
## using Buffer::equals here
|
||||
img1.bitmap.data.equals(img2.bitmap.data)
|
||||
|
||||
lastImagesAreDifferent = (data, image) ->
|
||||
## ensure the previous image isn't the same,
|
||||
## which might indicate the page has not scrolled yet
|
||||
previous = _.last(multipartImages)
|
||||
if not previous
|
||||
debug("no previous image to compare")
|
||||
return true
|
||||
|
||||
prevHash = previous.image.hash()
|
||||
currHash = image.hash()
|
||||
matches = prevHash is currHash
|
||||
matches = imagesMatch(previous.image, image)
|
||||
|
||||
debug("comparing previous and current image hashes %o", {
|
||||
prevHash
|
||||
currHash
|
||||
debug("comparing previous and current image pixels %o", {
|
||||
previous: previous.__ID__
|
||||
matches
|
||||
})
|
||||
|
||||
return prevHash isnt currHash
|
||||
## return whether or not the two images match
|
||||
## should be true if they don't, false if they do
|
||||
return not matches
|
||||
|
||||
multipartConditionFn = (data, image) ->
|
||||
if data.current is 1
|
||||
pixelConditionFn(data, image) and compareLast(data, image)
|
||||
pixelConditionFn(data, image) and lastImagesAreDifferent(data, image)
|
||||
else
|
||||
compareLast(data, image)
|
||||
lastImagesAreDifferent(data, image)
|
||||
|
||||
stitchScreenshots = (pixelRatio) ->
|
||||
width = Math.min(_.map(multipartImages, "data.clip.width")...)
|
||||
height = _.sumBy(multipartImages, "data.clip.height")
|
||||
fullWidth = _
|
||||
.chain(multipartImages)
|
||||
.map("data.clip.width")
|
||||
.min()
|
||||
.multiply(pixelRatio)
|
||||
.value()
|
||||
|
||||
fullHeight = _
|
||||
.chain(multipartImages)
|
||||
.sumBy("data.clip.height")
|
||||
.multiply(pixelRatio)
|
||||
.value()
|
||||
|
||||
debug("stitch #{multipartImages.length} images together")
|
||||
|
||||
takenAts = []
|
||||
|
||||
fullImage = new Jimp(width, height)
|
||||
heightMarker = 0
|
||||
fullImage = new Jimp(fullWidth, fullHeight)
|
||||
|
||||
_.each multipartImages, ({ data, image, takenAt }) ->
|
||||
croppedImage = image.clone()
|
||||
crop(croppedImage, data.clip, pixelRatio)
|
||||
croppedImage = crop(image, data.clip, pixelRatio)
|
||||
|
||||
debug("stitch: add image at (0, #{heightMarker})")
|
||||
|
||||
@@ -212,26 +273,28 @@ stitchScreenshots = (pixelRatio) ->
|
||||
|
||||
return { image: fullImage, takenAt: takenAts }
|
||||
|
||||
isBuffer = (details) ->
|
||||
!!details.buffer
|
||||
|
||||
getType = (details) ->
|
||||
if isBuffer(details)
|
||||
if details.buffer
|
||||
details.buffer.type
|
||||
else
|
||||
details.image.getMIME()
|
||||
|
||||
getBuffer = (details) ->
|
||||
if isBuffer(details)
|
||||
if details.buffer
|
||||
Promise.resolve(details.buffer)
|
||||
else
|
||||
details.image.getBuffer(Jimp.AUTO)
|
||||
Promise
|
||||
.promisify(details.image.getBuffer)
|
||||
.call(details.image, Jimp.AUTO)
|
||||
|
||||
getDimensions = (details) ->
|
||||
if isBuffer(details)
|
||||
sizeOf(details.buffer)
|
||||
pick = (obj) ->
|
||||
_.pick(obj, "width", "height")
|
||||
|
||||
if details.buffer
|
||||
pick(sizeOf(details.buffer))
|
||||
else
|
||||
_.pick(details.image.bitmap, "width", "height")
|
||||
pick(details.image.bitmap)
|
||||
|
||||
ensureUniquePath = (takenPaths, withoutExt, extension) ->
|
||||
fullPath = "#{withoutExt}.#{extension}"
|
||||
@@ -270,6 +333,8 @@ module.exports = {
|
||||
|
||||
clearMultipartState
|
||||
|
||||
imagesMatch
|
||||
|
||||
copy: (src, dest) ->
|
||||
fs
|
||||
.copyAsync(src, dest, {overwrite: true})
|
||||
@@ -320,7 +385,9 @@ module.exports = {
|
||||
if data.current is 1
|
||||
clearMultipartState()
|
||||
|
||||
multipartImages.push({ data, image, takenAt })
|
||||
debug("storing image for future comparison", __ID__)
|
||||
|
||||
multipartImages.push({ data, image, takenAt, __ID__ })
|
||||
|
||||
if data.current is data.total
|
||||
{ image } = stitchScreenshots(pixelRatio)
|
||||
@@ -330,14 +397,14 @@ module.exports = {
|
||||
return {}
|
||||
|
||||
if isAppOnly(data) or isMultipart(data)
|
||||
crop(image, data.clip, pixelRatio)
|
||||
image = crop(image, data.clip, pixelRatio)
|
||||
|
||||
return { image, pixelRatio, multipart, takenAt }
|
||||
.then ({ image, pixelRatio, multipart, takenAt }) ->
|
||||
return null if not image
|
||||
|
||||
if image and data.userClip
|
||||
crop(image, data.userClip, pixelRatio)
|
||||
image = crop(image, data.userClip, pixelRatio)
|
||||
|
||||
return { image, pixelRatio, multipart, takenAt }
|
||||
|
||||
@@ -357,6 +424,7 @@ module.exports = {
|
||||
{ multipart, pixelRatio, takenAt } = details
|
||||
|
||||
{
|
||||
size
|
||||
takenAt
|
||||
dimensions
|
||||
multipart
|
||||
@@ -364,7 +432,6 @@ module.exports = {
|
||||
name: data.name
|
||||
specName: data.specName
|
||||
testFailure: data.testFailure
|
||||
size: bytes(size, {unitSeparator: " "})
|
||||
path: pathToScreenshot
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ class Socket
|
||||
socket.on "automation:response", automation.response
|
||||
|
||||
socket.on "automation:request", (message, data, cb) =>
|
||||
log("automation:request", message, data)
|
||||
log("automation:request %o", message, data)
|
||||
|
||||
automationRequest(message, data)
|
||||
.then (resp) ->
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"babelify": "^7.3.0",
|
||||
"bluebird": "3.4.7",
|
||||
"browserify": "^13.1.1",
|
||||
"bytes": "^2.4.0",
|
||||
"chai": "^1.9.2",
|
||||
"chalk": "^2.4.1",
|
||||
"check-more-types": "^2.24.0",
|
||||
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 496 KiB |
@@ -1,6 +1,15 @@
|
||||
{ devicePixelRatio } = window
|
||||
|
||||
describe "taking screenshots", ->
|
||||
onAfterScreenshotResults = []
|
||||
|
||||
Cypress.Screenshot.defaults({
|
||||
onAfterScreenshot: ($el, results) ->
|
||||
onAfterScreenshotResults.push(results)
|
||||
})
|
||||
|
||||
failureTestRan = false
|
||||
|
||||
it "manually generates pngs", ->
|
||||
cy
|
||||
.visit("http://localhost:3322/color/black")
|
||||
@@ -15,12 +24,27 @@ describe "taking screenshots", ->
|
||||
.screenshot("foo/bar/baz", { capture: "runner" })
|
||||
|
||||
it "generates pngs on failure", ->
|
||||
failureTestRan = true
|
||||
|
||||
cy
|
||||
.visit("http://localhost:3322/color/yellow")
|
||||
.wait(1500)
|
||||
.then ->
|
||||
## failure 1
|
||||
throw new Error("fail whale")
|
||||
|
||||
it "does not call onAfterScreenshot with results of failed tests", ->
|
||||
## this test will only pass if the previous test ran
|
||||
if not failureTestRan
|
||||
throw new Error("this test can only pass if the previous test ran")
|
||||
|
||||
testFailure = Cypress._.find(onAfterScreenshotResults, { testFailure: true })
|
||||
|
||||
expect(testFailure).not.to.exist
|
||||
|
||||
expect(Cypress._.map(onAfterScreenshotResults, "name")).to.deep.eq([
|
||||
"black", "red", "foo/bar/baz"
|
||||
])
|
||||
|
||||
it "handles devicePixelRatio correctly on headless electron", ->
|
||||
## this checks to see if the topLeftRight pixel (1, 0) is
|
||||
@@ -101,18 +125,25 @@ describe "taking screenshots", ->
|
||||
cy
|
||||
.viewport(400, 400)
|
||||
.visit("http://localhost:3322/identical")
|
||||
.get("div:first").should("have.css", "height", "1300px")
|
||||
.screenshot({
|
||||
onAfterScreenshot: ($el, results) ->
|
||||
expect($el).to.match("div")
|
||||
|
||||
{ duration } = results
|
||||
|
||||
## there should be 4 screenshots taken
|
||||
## because the height is 1300px.
|
||||
## the first will resolve super fast
|
||||
## but the other 3 will take at least 1500ms
|
||||
## but not much more!
|
||||
first = 500
|
||||
total = first + (1500 * 3)
|
||||
padding = 1000 * 3 ## account for exceeding 1500
|
||||
## because the height is 1700px.
|
||||
## the 1st will resolve super fast since it
|
||||
## won't match any other screenshots.
|
||||
## the 2th/3rd will take up to their 1500ms
|
||||
## because they will be identical to the first.
|
||||
## the 4th will also go quickly because it will not
|
||||
## match the 3rd
|
||||
first = fourth = 250
|
||||
second = third = 1500
|
||||
total = first + second + third + fourth
|
||||
padding = 2000 ## account for slower machines
|
||||
|
||||
expect(duration).to.be.within(total, total + padding)
|
||||
})
|
||||
@@ -134,7 +165,9 @@ describe "taking screenshots", ->
|
||||
cy
|
||||
.viewport(600, 200)
|
||||
.visit("http://localhost:3322/color/yellow")
|
||||
.screenshot("app-clip", { capture: "viewport", clip: { x: 10, y: 10, width: 100, height: 50 }})
|
||||
.screenshot("app-clip", {
|
||||
capture: "viewport", clip: { x: 10, y: 10, width: 100, height: 50 }
|
||||
})
|
||||
.task("check:screenshot:size", {
|
||||
name: "screenshots_spec.coffee/app-clip.png",
|
||||
width: 100,
|
||||
@@ -146,7 +179,9 @@ describe "taking screenshots", ->
|
||||
cy
|
||||
.viewport(600, 200)
|
||||
.visit("http://localhost:3322/color/yellow")
|
||||
.screenshot("runner-clip", { capture: "runner", clip: { x: 15, y: 15, width: 120, height: 60 }})
|
||||
.screenshot("runner-clip", {
|
||||
capture: "runner", clip: { x: 15, y: 15, width: 120, height: 60 }
|
||||
})
|
||||
.task("check:screenshot:size", {
|
||||
name: "screenshots_spec.coffee/runner-clip.png",
|
||||
width: 120,
|
||||
@@ -158,7 +193,9 @@ describe "taking screenshots", ->
|
||||
cy
|
||||
.viewport(600, 200)
|
||||
.visit("http://localhost:3322/fullPage")
|
||||
.screenshot("fullPage-clip", { capture: "fullPage", clip: { x: 20, y: 20, width: 140, height: 70 }})
|
||||
.screenshot("fullPage-clip", {
|
||||
capture: "fullPage", clip: { x: 20, y: 20, width: 140, height: 70 }
|
||||
})
|
||||
.task("check:screenshot:size", {
|
||||
name: "screenshots_spec.coffee/fullPage-clip.png",
|
||||
width: 140,
|
||||
@@ -171,7 +208,9 @@ describe "taking screenshots", ->
|
||||
.viewport(600, 200)
|
||||
.visit("http://localhost:3322/element")
|
||||
.get(".element")
|
||||
.screenshot("element-clip", { clip: { x: 25, y: 25, width: 160, height: 80 }})
|
||||
.screenshot("element-clip", {
|
||||
clip: { x: 25, y: 25, width: 160, height: 80 }
|
||||
})
|
||||
.task("check:screenshot:size", {
|
||||
name: "screenshots_spec.coffee/element-clip.png",
|
||||
width: 160,
|
||||
|
||||
@@ -8,6 +8,7 @@ preprocessor = require("#{root}../../lib/plugins/child/preprocessor")
|
||||
task = require("#{root}../../lib/plugins/child/task")
|
||||
runPlugins = require("#{root}../../lib/plugins/child/run_plugins")
|
||||
util = require("#{root}../../lib/plugins/util")
|
||||
Fixtures = require("#{root}../../test/support/helpers/fixtures")
|
||||
|
||||
colorCodeRe = /\[[0-9;]+m/gm
|
||||
pathRe = /\/?([a-z0-9_-]+\/)*[a-z0-9_-]+\/([a-z_]+\.\w+)[:0-9]+/gmi
|
||||
@@ -38,14 +39,20 @@ describe "lib/plugins/child/run_plugins", ->
|
||||
|
||||
it "sends error message if requiring pluginsFile errors", ->
|
||||
## path for substitute is relative to lib/plugins/child/plugins_child.js
|
||||
mockery.registerSubstitute("plugins-file", "../../../test/fixtures/throws_error.coffee")
|
||||
mockery.registerSubstitute(
|
||||
"plugins-file",
|
||||
Fixtures.path("server/throws_error.coffee")
|
||||
)
|
||||
runPlugins(@ipc, "plugins-file")
|
||||
expect(@ipc.send).to.be.calledWith("load:error", "PLUGINS_FILE_ERROR", "plugins-file")
|
||||
snapshot(withoutStackPaths(@ipc.send.lastCall.args[3]))
|
||||
|
||||
it "sends error message if pluginsFile has syntax error", ->
|
||||
## path for substitute is relative to lib/plugins/child/plugins_child.js
|
||||
mockery.registerSubstitute("plugins-file", "../../../test/fixtures/syntax_error.coffee")
|
||||
mockery.registerSubstitute(
|
||||
"plugins-file",
|
||||
Fixtures.path("server/syntax_error.coffee")
|
||||
)
|
||||
runPlugins(@ipc, "plugins-file")
|
||||
expect(@ipc.send).to.be.calledWith("load:error", "PLUGINS_FILE_ERROR", "plugins-file")
|
||||
snapshot(withoutColorCodes(withoutPath(@ipc.send.lastCall.args[3])))
|
||||
|
||||
@@ -31,15 +31,16 @@ describe "lib/screenshots", ->
|
||||
viewport: { width: 40, height: 40 }
|
||||
}
|
||||
|
||||
@buffer = {}
|
||||
@buffer = new Buffer("image 1 data buffer")
|
||||
|
||||
@jimpImage = {
|
||||
id: 1
|
||||
bitmap: {
|
||||
width: 40
|
||||
height: 40
|
||||
data: @buffer
|
||||
}
|
||||
crop: sinon.stub()
|
||||
crop: sinon.stub().callsFake => @jimpImage
|
||||
getBuffer: sinon.stub().resolves(@buffer)
|
||||
getMIME: -> "image/png"
|
||||
hash: sinon.stub().returns("image hash")
|
||||
@@ -47,7 +48,7 @@ describe "lib/screenshots", ->
|
||||
}
|
||||
|
||||
Jimp.prototype.composite = sinon.stub()
|
||||
Jimp.prototype.getBuffer = sinon.stub().resolves(@buffer)
|
||||
# Jimp.prototype.getBuffer = sinon.stub().resolves(@buffer)
|
||||
|
||||
config.get(@todosPath).then (@config) =>
|
||||
|
||||
@@ -160,19 +161,28 @@ describe "lib/screenshots", ->
|
||||
|
||||
@getPixelColor.withArgs(0, 0).onSecondCall().returns("white")
|
||||
|
||||
@jimpImage2 = _.extend({}, @jimpImage, {
|
||||
clone = (img, props) ->
|
||||
_.defaultsDeep(props, img)
|
||||
|
||||
@jimpImage2 = clone(@jimpImage, {
|
||||
id: 2
|
||||
hash: sinon.stub().returns("image 2 hash")
|
||||
bitmap: {
|
||||
data: new Buffer("image 2 data buffer")
|
||||
}
|
||||
})
|
||||
|
||||
@jimpImage3 = _.extend({}, @jimpImage, {
|
||||
@jimpImage3 = clone(@jimpImage, {
|
||||
id: 3
|
||||
hash: sinon.stub().returns("image 3 hash")
|
||||
bitmap: {
|
||||
data: new Buffer("image 3 data buffer")
|
||||
}
|
||||
})
|
||||
|
||||
@jimpImage4 = _.extend({}, @jimpImage, {
|
||||
@jimpImage4 = clone(@jimpImage, {
|
||||
id: 4
|
||||
hash: sinon.stub().returns("image 4 hash")
|
||||
bitmap: {
|
||||
data: new Buffer("image 4 data buffer")
|
||||
}
|
||||
})
|
||||
|
||||
it "retries until helper pixels are no longer present on first capture", ->
|
||||
@@ -190,12 +200,9 @@ describe "lib/screenshots", ->
|
||||
.then =>
|
||||
expect(@automate.callCount).to.equal(4)
|
||||
|
||||
## image.hash() is very expensive and we want to make sure its only called
|
||||
## once for each image
|
||||
expect(@jimpImage2.hash).to.be.calledOnce
|
||||
|
||||
it "resolves no image on non-last captures", ->
|
||||
screenshots.capture(@appData, @automate).then (image) ->
|
||||
screenshots.capture(@appData, @automate)
|
||||
.then (image) ->
|
||||
expect(image).to.be.null
|
||||
|
||||
it "resolves details w/ image on last capture", ->
|
||||
@@ -257,6 +264,89 @@ describe "lib/screenshots", ->
|
||||
.then ->
|
||||
expect(Jimp.prototype.composite).not.to.be.called
|
||||
|
||||
describe "integration", ->
|
||||
beforeEach ->
|
||||
screenshots.clearMultipartState()
|
||||
|
||||
@currentTest.timeout(10000)
|
||||
|
||||
sinon.restore()
|
||||
|
||||
@data1 = {
|
||||
titles: [ 'cy.screenshot() - take a screenshot' ],
|
||||
testId: 'r2',
|
||||
name: 'app-screenshot',
|
||||
capture: 'fullPage',
|
||||
clip: { x: 0, y: 0, width: 1000, height: 646 },
|
||||
viewport: { width: 1280, height: 646 },
|
||||
current: 1,
|
||||
total: 3
|
||||
}
|
||||
|
||||
@data2 = {
|
||||
titles: [ 'cy.screenshot() - take a screenshot' ],
|
||||
testId: 'r2',
|
||||
name: 'app-screenshot',
|
||||
capture: 'fullPage',
|
||||
clip: { x: 0, y: 0, width: 1000, height: 646 },
|
||||
viewport: { width: 1280, height: 646 },
|
||||
current: 2,
|
||||
total: 3
|
||||
}
|
||||
|
||||
@data3 = {
|
||||
titles: [ 'cy.screenshot() - take a screenshot' ],
|
||||
testId: 'r2',
|
||||
name: 'app-screenshot',
|
||||
capture: 'fullPage',
|
||||
clip: { x: 0, y: 138, width: 1000, height: 508 },
|
||||
viewport: { width: 1280, height: 646 },
|
||||
current: 3,
|
||||
total: 3
|
||||
}
|
||||
|
||||
@dataUri = (img) ->
|
||||
return ->
|
||||
fs.readFileAsync(Fixtures.path("img/#{img}"))
|
||||
.then (buf) ->
|
||||
"data:image/png;base64," + buf.toString("base64")
|
||||
|
||||
it "stiches together 1x DPI images", ->
|
||||
screenshots
|
||||
.capture(@data1, @dataUri("DPI-1x/1.png"))
|
||||
.then (img1) =>
|
||||
expect(img1).to.be.null
|
||||
|
||||
screenshots
|
||||
.capture(@data2, @dataUri("DPI-1x/2.png"))
|
||||
.then (img2) =>
|
||||
expect(img2).to.be.null
|
||||
|
||||
screenshots
|
||||
.capture(@data3, @dataUri("DPI-1x/3.png"))
|
||||
.then (img3) =>
|
||||
Jimp.read(Fixtures.path("img/DPI-1x/stitched.png"))
|
||||
.then (img) =>
|
||||
expect(screenshots.imagesMatch(img, img3.image))
|
||||
|
||||
it "stiches together 2x DPI images", ->
|
||||
screenshots
|
||||
.capture(@data1, @dataUri("DPI-2x/1.png"))
|
||||
.then (img1) =>
|
||||
expect(img1).to.be.null
|
||||
|
||||
screenshots
|
||||
.capture(@data2, @dataUri("DPI-2x/2.png"))
|
||||
.then (img2) =>
|
||||
expect(img2).to.be.null
|
||||
|
||||
screenshots
|
||||
.capture(@data3, @dataUri("DPI-2x/3.png"))
|
||||
.then (img3) =>
|
||||
Jimp.read(Fixtures.path("img/DPI-2x/stitched.png"))
|
||||
.then (img) =>
|
||||
expect(screenshots.imagesMatch(img, img3.image))
|
||||
|
||||
context ".crop", ->
|
||||
beforeEach ->
|
||||
@dimensions = (overrides) ->
|
||||
@@ -288,26 +378,46 @@ describe "lib/screenshots", ->
|
||||
|
||||
context ".save", ->
|
||||
it "outputs file and returns details", ->
|
||||
details = {
|
||||
image: @jimpImage
|
||||
multipart: false
|
||||
pixelRatio: 2
|
||||
takenAt: "taken:at:date"
|
||||
}
|
||||
buf = dataUriToBuffer(image)
|
||||
|
||||
screenshots.save({name: "foo bar\\baz%/my-$screenshot"}, details, @config.screenshotsFolder)
|
||||
.then (result) =>
|
||||
expectedPath = path.join(@config.screenshotsFolder, "foo bar", "baz", "my-screenshot.png")
|
||||
actualPath = path.normalize(result.path)
|
||||
Jimp.read(buf)
|
||||
.then (i) =>
|
||||
details = {
|
||||
image: i
|
||||
multipart: false
|
||||
pixelRatio: 2
|
||||
takenAt: "1234-date"
|
||||
}
|
||||
|
||||
expect(actualPath).to.eq(expectedPath)
|
||||
expect(result.size).to.eq("15 B")
|
||||
expect(result.dimensions).to.eql({ width: 40, height: 40 })
|
||||
expect(result.multipart).to.be.false
|
||||
expect(result.pixelRatio).to.be.eq(2)
|
||||
expect(result.takenAt).to.eq("taken:at:date")
|
||||
dimensions = sizeOf(buf)
|
||||
|
||||
fs.statAsync(expectedPath)
|
||||
screenshots.save(
|
||||
{ name: "foo bar\\baz%/my-$screenshot", specName: "foo.spec.js", testFailure: false },
|
||||
details,
|
||||
@config.screenshotsFolder
|
||||
)
|
||||
.then (result) =>
|
||||
expectedPath = path.join(
|
||||
@config.screenshotsFolder, "foo.spec.js", "foo bar", "baz", "my-screenshot.png"
|
||||
)
|
||||
|
||||
actualPath = path.normalize(result.path)
|
||||
|
||||
expect(result).to.deep.eq({
|
||||
multipart: false
|
||||
pixelRatio: 2
|
||||
path: path.normalize(result.path)
|
||||
size: 284
|
||||
name: "foo bar\\baz%/my-$screenshot"
|
||||
specName: "foo.spec.js"
|
||||
testFailure: false
|
||||
takenAt: "1234-date"
|
||||
dimensions: _.pick(dimensions, "width", "height")
|
||||
})
|
||||
|
||||
expect(expectedPath).to.eq(actualPath)
|
||||
|
||||
fs.statAsync(expectedPath)
|
||||
|
||||
it "can handle saving buffer", ->
|
||||
details = {
|
||||
@@ -316,8 +426,9 @@ describe "lib/screenshots", ->
|
||||
buffer: dataUriToBuffer(image)
|
||||
takenAt: "1234-date"
|
||||
}
|
||||
|
||||
|
||||
dimensions = sizeOf(details.buffer)
|
||||
|
||||
screenshots.save(
|
||||
{ name: "with-buffer", specName: "foo.spec.js", testFailure: false },
|
||||
details,
|
||||
@@ -327,21 +438,21 @@ describe "lib/screenshots", ->
|
||||
expectedPath = path.join(
|
||||
@config.screenshotsFolder, "foo.spec.js", "with-buffer.png"
|
||||
)
|
||||
|
||||
|
||||
actualPath = path.normalize(result.path)
|
||||
|
||||
|
||||
expect(result).to.deep.eq({
|
||||
dimensions
|
||||
name: "with-buffer"
|
||||
multipart: false
|
||||
pixelRatio: 1
|
||||
path: path.normalize(result.path)
|
||||
size: "279 B"
|
||||
size: 279
|
||||
specName: "foo.spec.js"
|
||||
testFailure: false
|
||||
takenAt: "1234-date"
|
||||
dimensions: _.pick(dimensions, "width", "height")
|
||||
})
|
||||
|
||||
|
||||
expect(expectedPath).to.eq(actualPath)
|
||||
|
||||
fs.statAsync(expectedPath)
|
||||
@@ -366,14 +477,14 @@ describe "lib/screenshots", ->
|
||||
expect(p).to.eq(
|
||||
"path/to/screenshots/examples$/user/list.js/quux/lorem.png"
|
||||
)
|
||||
|
||||
|
||||
p2 = screenshots.getPath({
|
||||
specName: "examples$/user/list.js"
|
||||
titles: ["bar", "baz"]
|
||||
name: "quux*"
|
||||
takenPaths: ["path/to/screenshots/examples$/user/list.js/quux.png"]
|
||||
}, "png", "path/to/screenshots")
|
||||
|
||||
|
||||
expect(p2).to.eq(
|
||||
"path/to/screenshots/examples$/user/list.js/quux (1).png"
|
||||
)
|
||||
@@ -389,13 +500,13 @@ describe "lib/screenshots", ->
|
||||
expect(p).to.eq(
|
||||
"path/to/screenshots/examples$/user/list.js/bar -- baz (failed).png"
|
||||
)
|
||||
|
||||
|
||||
p2 = screenshots.getPath({
|
||||
specName: "examples$/user/list.js"
|
||||
titles: ["bar", "baz^"]
|
||||
takenPaths: ["path/to/screenshots/examples$/user/list.js/bar -- baz.png"]
|
||||
}, "png", "path/to/screenshots")
|
||||
|
||||
|
||||
expect(p2).to.eq(
|
||||
"path/to/screenshots/examples$/user/list.js/bar -- baz (1).png"
|
||||
)
|
||||
|
||||