screenshot perf and high dpi res (#1864)

- fixes #1857 
- fixes #1863
This commit is contained in:
Brian Mann
2018-06-18 03:15:02 -04:00
committed by GitHub
parent fc1cf4a144
commit 98fc4bc83c
21 changed files with 421 additions and 118 deletions
+84 -10
View File
@@ -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
+1
View File
@@ -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 - -
`
+107 -40
View File
@@ -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
}
+1 -1
View File
@@ -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) ->
-1
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

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])))
+152 -41
View File
@@ -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"
)