diff --git a/circle.yml b/circle.yml index 7a5c1d5fc4..b53ec94474 100644 --- a/circle.yml +++ b/circle.yml @@ -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 diff --git a/packages/driver/package.json b/packages/driver/package.json index 0c28d5694b..18c3f671a3 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -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", diff --git a/packages/driver/src/cy/commands/screenshot.coffee b/packages/driver/src/cy/commands/screenshot.coffee index 589f0fb525..37fd68f52a 100644 --- a/packages/driver/src/cy/commands/screenshot.coffee +++ b/packages/driver/src/cy/commands/screenshot.coffee @@ -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" }) diff --git a/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee b/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee index c0891347c2..e5f04c3814 100644 --- a/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee @@ -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$/) diff --git a/packages/server/__snapshots__/screenshots_spec.coffee b/packages/server/__snapshots__/screenshots_spec.coffee index c3353aaf9d..56f6a048db 100644 --- a/packages/server/__snapshots__/screenshots_spec.coffee +++ b/packages/server/__snapshots__/screenshots_spec.coffee @@ -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 - - ` diff --git a/packages/server/lib/screenshots.coffee b/packages/server/lib/screenshots.coffee index 9e26f7cfc4..5e69921aac 100644 --- a/packages/server/lib/screenshots.coffee +++ b/packages/server/lib/screenshots.coffee @@ -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 } diff --git a/packages/server/lib/socket.coffee b/packages/server/lib/socket.coffee index 5c335d34e5..d5e7aaeaaf 100644 --- a/packages/server/lib/socket.coffee +++ b/packages/server/lib/socket.coffee @@ -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) -> diff --git a/packages/server/package.json b/packages/server/package.json index f110e481fd..8dda38e899 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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", diff --git a/packages/server/test/support/fixtures/img/DPI-1x/1.png b/packages/server/test/support/fixtures/img/DPI-1x/1.png new file mode 100644 index 0000000000..93103ecf82 Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-1x/1.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-1x/2.png b/packages/server/test/support/fixtures/img/DPI-1x/2.png new file mode 100644 index 0000000000..610e1b253f Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-1x/2.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-1x/3.png b/packages/server/test/support/fixtures/img/DPI-1x/3.png new file mode 100644 index 0000000000..fc6ca0b5d8 Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-1x/3.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-1x/stitched.png b/packages/server/test/support/fixtures/img/DPI-1x/stitched.png new file mode 100644 index 0000000000..1d415c28e9 Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-1x/stitched.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-2x/1.png b/packages/server/test/support/fixtures/img/DPI-2x/1.png new file mode 100644 index 0000000000..aff038c996 Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-2x/1.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-2x/2.png b/packages/server/test/support/fixtures/img/DPI-2x/2.png new file mode 100644 index 0000000000..c56983545c Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-2x/2.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-2x/3.png b/packages/server/test/support/fixtures/img/DPI-2x/3.png new file mode 100644 index 0000000000..7afda2b8a5 Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-2x/3.png differ diff --git a/packages/server/test/support/fixtures/img/DPI-2x/stitched.png b/packages/server/test/support/fixtures/img/DPI-2x/stitched.png new file mode 100644 index 0000000000..9ce366dff2 Binary files /dev/null and b/packages/server/test/support/fixtures/img/DPI-2x/stitched.png differ diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee index 203bf4760c..a82fd70edc 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee @@ -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, diff --git a/packages/server/test/fixtures/syntax_error.coffee b/packages/server/test/support/fixtures/server/syntax_error.coffee similarity index 100% rename from packages/server/test/fixtures/syntax_error.coffee rename to packages/server/test/support/fixtures/server/syntax_error.coffee diff --git a/packages/server/test/fixtures/throws_error.coffee b/packages/server/test/support/fixtures/server/throws_error.coffee similarity index 100% rename from packages/server/test/fixtures/throws_error.coffee rename to packages/server/test/support/fixtures/server/throws_error.coffee diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee index 37fbb13de3..8a030398dd 100644 --- a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee +++ b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee @@ -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]))) diff --git a/packages/server/test/unit/screenshots_spec.coffee b/packages/server/test/unit/screenshots_spec.coffee index 63242d4caa..640b942958 100644 --- a/packages/server/test/unit/screenshots_spec.coffee +++ b/packages/server/test/unit/screenshots_spec.coffee @@ -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" )