From af4f4232953d22678871c6b5f343c1676954da37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Tue, 20 Aug 2019 15:00:38 +0200 Subject: [PATCH 01/16] error handling --- src/server/utils/error.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/server/utils/error.ts diff --git a/src/server/utils/error.ts b/src/server/utils/error.ts new file mode 100644 index 0000000..bf4314a --- /dev/null +++ b/src/server/utils/error.ts @@ -0,0 +1,21 @@ +import kill from 'tree-kill' +import { takeScreenshot, processId } from '../' + +export const killProcessAndThrowError = (error: Error) => { + if (processId) kill(processId) + throw error +} + +export const throwErrorWithScreenshot = (error: Error) => { + // Try to take screenshot if possible + const screenshotPath = `${__dirname}/${new Date()}.png` + return new Promise(resolve => { + takeScreenshot(screenshotPath) + .then(() => { + + }) + .catch(() => killProcessAndThrowError(error) ) + }) + + return +} \ No newline at end of file From 6b19bc62f8133b6dcf4541bd20ec6b75e0b35b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Tue, 27 Aug 2019 15:37:45 +0200 Subject: [PATCH 02/16] screenshot error handling --- jest.config.js | 1 + package-lock.json | 6 ++++++ package.json | 1 + src/server/index.ts | 11 +++++----- src/server/utils/error.ts | 32 +++++++++++++++++++++-------- test/local/helloworld.spark.test.js | 1 + test/remote/coverflow.spark.test.js | 8 ++++---- 7 files changed, 42 insertions(+), 18 deletions(-) diff --git a/jest.config.js b/jest.config.js index 8fcc29f..9147e1f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ module.exports = { setupFilesAfterEnv: ['./jest.setup.js'], + reporters: ['default', 'jest-html-reporters'], } diff --git a/package-lock.json b/package-lock.json index 3137b03..6116cd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5813,6 +5813,12 @@ "walker": "^1.0.7" } }, + "jest-html-reporters": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/jest-html-reporters/-/jest-html-reporters-1.1.8.tgz", + "integrity": "sha512-vNDfEJInonUYlMZxcZiO87CIz9GWU30wueHYaDhJJyY/aADMfL/9R1v2ZCrFqRtHhCZVgAIxnH21LMNRH4VIGg==", + "dev": true + }, "jest-jasmine2": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", diff --git a/package.json b/package.json index 9e6431e..9a3b549 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-react": "^7.14.3", "jest": "^24.8.0", + "jest-html-reporters": "^1.1.8", "prettier": "^1.18.2", "standard-version": "^7.0.0", "typescript": "^3.5.3" diff --git a/src/server/index.ts b/src/server/index.ts index bf9c718..04fc860 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,6 +8,7 @@ import kill from 'tree-kill' import request from 'request' import asyncRequest from 'request-promise' import { TestOptions, MessagePayload, SparkBrowserActions } from './types' +import { throwErrorWithScreenshot } from './utils/error' import { decodeBase64Image } from './utils/image' import { deepSearchMultiple } from './utils/search' import { @@ -104,7 +105,7 @@ export const initializeSparkTestBrowser = (testOptions: TestOptions = {}) => { websocketServer = new WebSocket.Server({ port: wsPort }) websocketServer.on('connection', ws => { - ws.on('message', (message: string) => { + ws.on('message', async (message: string) => { if (message) { const data = JSON.parse(message) if (data && data.connected && data.processId) { @@ -113,8 +114,7 @@ export const initializeSparkTestBrowser = (testOptions: TestOptions = {}) => { return resolve(data.processId) } if (data && data.uncaughtException) { - if (processId) kill(processId) - throw new Error(data.err) + await throwErrorWithScreenshot(data.err) } if (!data || !data.ticketId) throw new Error('Missing ticketId from client') @@ -211,6 +211,7 @@ export const closeBrowser = () => { return new Promise((resolve) => { if (!processId) resolve() kill(processId, () => { + processId = null resolve() }) }) @@ -265,10 +266,10 @@ const searchSceneTreeWithPropertyValue = ( if ((multiple && result.length) || (!multiple && result)) { clearTimeout(timeout) resolve(result) - break + return } } - resolve(multiple ? [] : null) + await throwErrorWithScreenshot(new Error(`Could not find element with property: ${property}, value:${value}`)) }) } diff --git a/src/server/utils/error.ts b/src/server/utils/error.ts index bf4314a..8eb1429 100644 --- a/src/server/utils/error.ts +++ b/src/server/utils/error.ts @@ -1,20 +1,34 @@ -import kill from 'tree-kill' -import { takeScreenshot, processId } from '../' +import { takeScreenshot, stopServerAndBrowser } from '../' -export const killProcessAndThrowError = (error: Error) => { - if (processId) kill(processId) +const imageThumbnail = (path: string) => (` + + + +`) + +const screenshotError = (path: string, originalError: Error) => { + const error = new Error() + error.stack = ` + ${originalError.stack} + ${imageThumbnail(path)} + ` + return error +} + +export const killProcessAndThrowError = async (error: Error) => { + await stopServerAndBrowser() throw error } -export const throwErrorWithScreenshot = (error: Error) => { +export const throwErrorWithScreenshot = async (error: Error) => { // Try to take screenshot if possible - const screenshotPath = `${__dirname}/${new Date()}.png` + const screenshotPath = `${process.cwd()}/test.png` return new Promise(resolve => { takeScreenshot(screenshotPath) - .then(() => { - + .then(async () => { + await killProcessAndThrowError(screenshotError(screenshotPath, error)) + resolve() }) - .catch(() => killProcessAndThrowError(error) ) }) return diff --git a/test/local/helloworld.spark.test.js b/test/local/helloworld.spark.test.js index d0fea53..52ee0b0 100644 --- a/test/local/helloworld.spark.test.js +++ b/test/local/helloworld.spark.test.js @@ -24,5 +24,6 @@ test('Should be able to assert on element', async done => { const element = await findElementWithPropertyValue('text', 'Hello World!') expect(element).toBeTruthy() expect(element.id).toBe('my-test-id') + const element1 = await findElementWithPropertyValue('text', 'findes ikke') done() }) diff --git a/test/remote/coverflow.spark.test.js b/test/remote/coverflow.spark.test.js index 47693b7..550cb76 100644 --- a/test/remote/coverflow.spark.test.js +++ b/test/remote/coverflow.spark.test.js @@ -43,12 +43,12 @@ test('Can test remote websites', async done => { await sendKeyCodeWithDelay('37', 1000) await sendKeyCodeWithDelay('13', 1000) // Assert element - const element1 = findElementWithPropertyValue('text', 'Christine') + const element1 = await findElementWithPropertyValue('text', 'Christine') expect(element1).toBeTruthy() await sendKeyCodeWithDelay('39', 1000) await sendKeyCodeWithDelay('13', 1000) // Assert element - const element2 = findElementWithPropertyValue('text', 'Deadpool') + const element2 = await findElementWithPropertyValue('text', 'Deadpool') expect(element2).toBeTruthy() done() }) @@ -71,12 +71,12 @@ test('Can test without delay', async done => { await sendKeyEvent('onKeyDown', '37') await sendKeyEvent('onKeyDown', '13') // Assert element - const element1 = findElementWithPropertyValue('text', 'Christine') + const element1 = await findElementWithPropertyValue('text', 'Christine') expect(element1).toBeTruthy() await sendKeyEvent('onKeyDown', '39') await sendKeyEvent('onKeyDown', '13') // Assert element - const element2 = findElementWithPropertyValue('text', 'Deadpool') + const element2 = await findElementWithPropertyValue('text', 'Deadpool') expect(element2).toBeTruthy() done() }) From a69a5d416df177c1ef39a70288ee8ee92bd7b2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 11:13:11 +0200 Subject: [PATCH 03/16] get ready for screenshot comparing --- jest.setup.js | 6 +- package-lock.json | 115 ++++++++++++++++++++++++++++++-------- package.json | 1 + src/client/index.js | 10 +++- src/index.ts | 4 ++ src/jest/imageExpect.ts | 5 ++ src/server/utils/error.ts | 5 +- 7 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 src/jest/imageExpect.ts diff --git a/jest.setup.js b/jest.setup.js index 9fd1957..0868b9a 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1 +1,5 @@ -jest.setTimeout(30000) \ No newline at end of file +import { toMatchImageSnapshot } from './dist' + +jest.setTimeout(30000) + +expect.extend({ toMatchImageSnapshot }) diff --git a/package-lock.json b/package-lock.json index 6116cd1..24dc703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1784,8 +1784,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -1883,7 +1882,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2195,8 +2193,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "2.0.0", @@ -2985,8 +2982,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.11.1", @@ -3835,8 +3831,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.9", @@ -4681,7 +4676,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4796,6 +4790,21 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4988,7 +4997,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4997,8 +5005,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -5819,6 +5826,62 @@ "integrity": "sha512-vNDfEJInonUYlMZxcZiO87CIz9GWU30wueHYaDhJJyY/aADMfL/9R1v2ZCrFqRtHhCZVgAIxnH21LMNRH4VIGg==", "dev": true }, + "jest-image-snapshot": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/jest-image-snapshot/-/jest-image-snapshot-2.9.0.tgz", + "integrity": "sha512-ih/DwdW2AWjQLgTZeaXDH1qMw7g346t6RUK+2KVGXX8c4tNpTinSKR3h059AWhFQGxlZpBZ/dds3DMigosUcdw==", + "requires": { + "chalk": "^1.1.3", + "get-stdin": "^5.0.1", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "pixelmatch": "^4.0.2", + "pngjs": "^3.3.3", + "rimraf": "^2.6.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "jest-jasmine2": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", @@ -6744,7 +6807,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6752,8 +6814,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minimist-options": { "version": "3.0.2", @@ -6790,7 +6851,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -7077,7 +7137,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -7272,8 +7331,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -7342,6 +7400,14 @@ "node-modules-regexp": "^1.0.0" } }, + "pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", + "requires": { + "pngjs": "^3.0.0" + } + }, "pkg-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", @@ -7368,6 +7434,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -7872,7 +7943,6 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, "requires": { "glob": "^7.1.3" } @@ -9048,8 +9118,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index 9a3b549..aab6ca9 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ }, "dependencies": { "express": "^4.17.1", + "jest-image-snapshot": "^2.9.0", "request": "^2.88.0", "request-promise": "^4.2.4", "tree-kill": "^1.2.1", diff --git a/src/client/index.js b/src/client/index.js index e2d1e77..30989b5 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -59,10 +59,11 @@ const websocketSendData = data => ), ) -const uploadImage = (image, http, payload) => { +const uploadImage = (image, http, payload, ticketId) => { const data = JSON.stringify({ pngImage: image, imagePathName: payload, + ticketId, }) return new Promise(function(resolve, reject) { const req = http.request( @@ -145,7 +146,7 @@ const handleServerResponse = (scene, data, http) => { http, payload, ticketId, - ).then(() => sendActionFullfilled(ticketId)) + ).then(() => websocketSendData({ ticketId, path: payload })) return } case 4: { @@ -167,7 +168,10 @@ const handleServerResponse = (scene, data, http) => { } case 5: { // Emit event - global.process.emit(payload.eventType, { keyCode: payload.keyCode, stopPropagation: () => null}) + global.process.emit(payload.eventType, { + keyCode: payload.keyCode, + stopPropagation: () => null, + }) sendActionFullfilled(ticketId) return } diff --git a/src/index.ts b/src/index.ts index 38d9ace..7790300 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,9 @@ import { findElementsWithPropertyValue, sendKeyEvent, } from './server' +import { + toMatchImageSnapshot +} from './jest/imageExpect' module.exports = { initializeSparkTestBrowser, @@ -18,4 +21,5 @@ module.exports = { findElementWithPropertyValue, findElementsWithPropertyValue, sendKeyEvent, + toMatchImageSnapshot, } diff --git a/src/jest/imageExpect.ts b/src/jest/imageExpect.ts new file mode 100644 index 0000000..338649e --- /dev/null +++ b/src/jest/imageExpect.ts @@ -0,0 +1,5 @@ +import { configureToMatchImageSnapshot } from 'jest-image-snapshot' + +export const toMatchImageSnapshot = (customConfig = {}) => { + return configureToMatchImageSnapshot(customConfig) +} diff --git a/src/server/utils/error.ts b/src/server/utils/error.ts index 8eb1429..1b661ea 100644 --- a/src/server/utils/error.ts +++ b/src/server/utils/error.ts @@ -6,10 +6,9 @@ const imageThumbnail = (path: string) => (` `) -const screenshotError = (path: string, originalError: Error) => { - const error = new Error() +const screenshotError = (path: string, error: Error) => { error.stack = ` - ${originalError.stack} + ${error.stack} ${imageThumbnail(path)} ` return error From 4a782f7fc2eef560b7dc62748a3678c0dd34907a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 15:24:11 +0200 Subject: [PATCH 04/16] image snapshot testing added --- .eslintrc.js | 3 +- jest.setup.js | 4 +- package-lock.json | 162 ++++++++++-------------- package.json | 8 +- src/client/index.js | 2 +- src/index.ts | 2 +- src/jest/diff-process.js | 23 ++++ src/jest/diff-snapshot.js | 249 +++++++++++++++++++++++++++++++++++++ src/jest/image-composer.js | 63 ++++++++++ src/jest/imageExpect.ts | 5 - src/jest/index.js | 244 ++++++++++++++++++++++++++++++++++++ src/server/index.ts | 19 ++- src/server/utils/error.ts | 4 +- 13 files changed, 674 insertions(+), 114 deletions(-) create mode 100644 src/jest/diff-process.js create mode 100644 src/jest/diff-snapshot.js create mode 100644 src/jest/image-composer.js delete mode 100644 src/jest/imageExpect.ts create mode 100644 src/jest/index.js diff --git a/.eslintrc.js b/.eslintrc.js index 1e9f74b..4d8aefa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,7 @@ module.exports = { 'fp/no-class': 'error', }, settings: { + 'import/core-modules': ['get-stdin', 'chalk'], 'import/resolver': { node: { extensions: ['.ts', '.js'], @@ -45,4 +46,4 @@ module.exports = { }, 'import/extensions': ['.ts', '.js'], }, -} \ No newline at end of file +} diff --git a/jest.setup.js b/jest.setup.js index 0868b9a..bbd00a4 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -2,4 +2,6 @@ import { toMatchImageSnapshot } from './dist' jest.setTimeout(30000) -expect.extend({ toMatchImageSnapshot }) +expect.extend({ + toMatchImageSnapshot, +}) diff --git a/package-lock.json b/package-lock.json index 24dc703..5e55cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -947,6 +947,17 @@ "realpath-native": "^1.1.0", "rimraf": "^2.5.4", "strip-ansi": "^5.0.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "@jest/environment": { @@ -1485,7 +1496,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -2034,7 +2044,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2149,7 +2158,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -2157,8 +2165,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -2744,6 +2751,15 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } } } }, @@ -3332,9 +3348,9 @@ } }, "eslint-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", - "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", + "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", "dev": true, "requires": { "eslint-visitor-keys": "^1.0.0" @@ -3764,6 +3780,17 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -4553,6 +4580,14 @@ "dev": true, "requires": { "get-stdin": "^4.0.1" + }, + "dependencies": { + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + } } }, "through2": { @@ -4574,10 +4609,9 @@ } }, "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==" }, "get-stream": { "version": "4.1.0", @@ -4790,26 +4824,10 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -5465,6 +5483,15 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5826,62 +5853,6 @@ "integrity": "sha512-vNDfEJInonUYlMZxcZiO87CIz9GWU30wueHYaDhJJyY/aADMfL/9R1v2ZCrFqRtHhCZVgAIxnH21LMNRH4VIGg==", "dev": true }, - "jest-image-snapshot": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/jest-image-snapshot/-/jest-image-snapshot-2.9.0.tgz", - "integrity": "sha512-ih/DwdW2AWjQLgTZeaXDH1qMw7g346t6RUK+2KVGXX8c4tNpTinSKR3h059AWhFQGxlZpBZ/dds3DMigosUcdw==", - "requires": { - "chalk": "^1.1.3", - "get-stdin": "^5.0.1", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "pixelmatch": "^4.0.2", - "pngjs": "^3.3.3", - "rimraf": "^2.6.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, "jest-jasmine2": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", @@ -6555,9 +6526,9 @@ } }, "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -7401,11 +7372,11 @@ } }, "pixelmatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", - "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.0.2.tgz", + "integrity": "sha512-b65UpTI40rGFY8QwN6IYuCbpmwAOL6M8d6voX4F3zR99UmDqh7r2QWLxoeHOazBRgEmDUdqNVESDREqFxQS7rQ==", "requires": { - "pngjs": "^3.0.0" + "pngjs": "^3.4.0" } }, "pkg-dir": { @@ -7940,9 +7911,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", "requires": { "glob": "^7.1.3" } @@ -8549,7 +8520,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } diff --git a/package.json b/package.json index aab6ca9..4c4d20c 100644 --- a/package.json +++ b/package.json @@ -63,10 +63,16 @@ "typescript": "^3.5.3" }, "dependencies": { + "chalk": "^2.4.2", "express": "^4.17.1", - "jest-image-snapshot": "^2.9.0", + "get-stdin": "^7.0.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "pixelmatch": "^5.0.2", + "pngjs": "^3.4.0", "request": "^2.88.0", "request-promise": "^4.2.4", + "rimraf": "^3.0.0", "tree-kill": "^1.2.1", "uid2": "0.0.3", "ws": "^7.1.0" diff --git a/src/client/index.js b/src/client/index.js index 30989b5..3a03b02 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -146,7 +146,7 @@ const handleServerResponse = (scene, data, http) => { http, payload, ticketId, - ).then(() => websocketSendData({ ticketId, path: payload })) + ).then(() => console.log('Screenshot done!')) return } case 4: { diff --git a/src/index.ts b/src/index.ts index 7790300..77e0232 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { } from './server' import { toMatchImageSnapshot -} from './jest/imageExpect' +} from './jest' module.exports = { initializeSparkTestBrowser, diff --git a/src/jest/diff-process.js b/src/jest/diff-process.js new file mode 100644 index 0000000..20a1eee --- /dev/null +++ b/src/jest/diff-process.js @@ -0,0 +1,23 @@ +import fs from 'fs' +import getStdin from 'get-stdin' +import { diffImageToSnapshot } from './diff-snapshot' + +getStdin.buffer().then(buffer => { + try { + const options = JSON.parse(buffer) + + options.receivedImageBuffer = Buffer.from( + options.receivedImageBuffer, + 'base64', + ) + + const result = diffImageToSnapshot(options) + + fs.writeSync(3, Buffer.from(JSON.stringify(result))) + + process.exit(0) + } catch (error) { + console.error(error) // eslint-disable-line no-console + process.exit(1) + } +}) diff --git a/src/jest/diff-snapshot.js b/src/jest/diff-snapshot.js new file mode 100644 index 0000000..bd36358 --- /dev/null +++ b/src/jest/diff-snapshot.js @@ -0,0 +1,249 @@ +import childProcess from 'child_process' +import fs from 'fs' +import path from 'path' +import mkdirp from 'mkdirp' +import pixelmatch from 'pixelmatch' +import { PNG } from 'pngjs' +import rimraf from 'rimraf' +import { createHash } from 'crypto' +import ImageComposer from './image-composer' + +/** + * Helper function to create reusable image resizer + */ +const createImageResizer = (width, height) => source => { + const resized = new PNG({ width, height, fill: true }) + PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0) + return resized +} + +/** + * Fills diff area with black transparent color for meaningful diff + */ +/* eslint-disable no-plusplus, no-param-reassign, no-bitwise */ +const fillSizeDifference = (width, height) => image => { + const inArea = (x, y) => y > height || x > width + for (let y = 0; y < image.height; y++) { + for (let x = 0; x < image.width; x++) { + if (inArea(x, y)) { + const idx = (image.width * y + x) << 2 + image.data[idx] = 0 + image.data[idx + 1] = 0 + image.data[idx + 2] = 0 + image.data[idx + 3] = 64 + } + } + } + return image +} +/* eslint-enabled */ + +/** + * Aligns images sizes to biggest common value + * and fills new pixels with transparent pixels + */ +const alignImagesToSameSize = (firstImage, secondImage) => { + // Keep original sizes to fill extended area later + const firstImageWidth = firstImage.width + const firstImageHeight = firstImage.height + const secondImageWidth = secondImage.width + const secondImageHeight = secondImage.height + // Calculate biggest common values + const resizeToSameSize = createImageResizer( + Math.max(firstImageWidth, secondImageWidth), + Math.max(firstImageHeight, secondImageHeight), + ) + // Resize both images + const resizedFirst = resizeToSameSize(firstImage) + const resizedSecond = resizeToSameSize(secondImage) + // Fill resized area with black transparent pixels + return [ + fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst), + fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond), + ] +} + +const isFailure = ({ pass, updateSnapshot }) => !pass && !updateSnapshot + +const shouldUpdate = ({ pass, updateSnapshot, updatePassedSnapshot }) => + (!pass && updateSnapshot) || (pass && updatePassedSnapshot) + +export const diffImageToSnapshot = options => { + const { + receivedImageBuffer, + snapshotIdentifier, + snapshotsDir, + diffDir, + diffDirection, + updateSnapshot = false, + updatePassedSnapshot = false, + customDiffConfig = {}, + failureThreshold, + failureThresholdType, + } = options + + let result = {} + const baselineSnapshotPath = path.join( + snapshotsDir, + `${snapshotIdentifier}-snap.png`, + ) + if (!fs.existsSync(baselineSnapshotPath)) { + mkdirp.sync(snapshotsDir) + fs.writeFileSync(baselineSnapshotPath, receivedImageBuffer) + result = { added: true } + } else { + const diffOutputPath = path.join(diffDir, `${snapshotIdentifier}-diff.png`) + rimraf.sync(diffOutputPath) + + const defaultDiffConfig = { + threshold: 0.01, + } + + const diffConfig = Object.assign({}, defaultDiffConfig, customDiffConfig) + + const rawReceivedImage = PNG.sync.read(receivedImageBuffer) + const rawBaselineImage = PNG.sync.read( + fs.readFileSync(baselineSnapshotPath), + ) + const hasSizeMismatch = + rawReceivedImage.height !== rawBaselineImage.height || + rawReceivedImage.width !== rawBaselineImage.width + const imageDimensions = { + receivedHeight: rawReceivedImage.height, + receivedWidth: rawReceivedImage.width, + baselineHeight: rawBaselineImage.height, + baselineWidth: rawBaselineImage.width, + } + // Align images in size if different + const [receivedImage, baselineImage] = hasSizeMismatch + ? alignImagesToSameSize(rawReceivedImage, rawBaselineImage) + : [rawReceivedImage, rawBaselineImage] + const imageWidth = receivedImage.width + const imageHeight = receivedImage.height + const diffImage = new PNG({ width: imageWidth, height: imageHeight }) + + let pass = false + let diffSize = false + let diffRatio = 0 + let diffPixelCount = 0 + + const receivedImageDigest = createHash('sha1') + .update(receivedImage.data) + .digest('base64') + const baselineImageDigest = createHash('sha1') + .update(baselineImage.data) + .digest('base64') + + pass = receivedImageDigest === baselineImageDigest + + if (!pass) { + diffPixelCount = pixelmatch( + receivedImage.data, + baselineImage.data, + diffImage.data, + imageWidth, + imageHeight, + diffConfig, + ) + + const totalPixels = imageWidth * imageHeight + diffRatio = diffPixelCount / totalPixels + // Always fail test on image size mismatch + if (hasSizeMismatch) { + pass = false + diffSize = true + } else if (failureThresholdType === 'pixel') { + pass = diffPixelCount <= failureThreshold + } else if (failureThresholdType === 'percent') { + pass = diffRatio <= failureThreshold + } else { + throw new Error( + `Unknown failureThresholdType: ${failureThresholdType}. Valid options are "pixel" or "percent".`, + ) + } + } + + if (isFailure({ pass, updateSnapshot })) { + mkdirp.sync(diffDir) + const composer = new ImageComposer({ + direction: diffDirection, + }) + + composer.addImage(baselineImage, imageWidth, imageHeight) + composer.addImage(diffImage, imageWidth, imageHeight) + composer.addImage(receivedImage, imageWidth, imageHeight) + + const composerParams = composer.getParams() + + const compositeResultImage = new PNG({ + width: composerParams.compositeWidth, + height: composerParams.compositeHeight, + }) + + // copy baseline, diff, and received images into composite result image + composerParams.images.forEach((image, index) => { + PNG.bitblt( + image.imageData, + compositeResultImage, + 0, + 0, + image.imageWidth, + image.imageHeight, + composerParams.offsetX * index, + composerParams.offsetY * index, + ) + }) + // Set filter type to Paeth to avoid expensive auto scanline filter detection + // For more information see https://www.w3.org/TR/PNG-Filters.html + const pngBuffer = PNG.sync.write(compositeResultImage, { filterType: 4 }) + fs.writeFileSync(diffOutputPath, pngBuffer) + + result = { + pass: false, + diffSize, + imageDimensions, + diffOutputPath, + diffRatio, + diffPixelCount, + } + } else if (shouldUpdate({ pass, updateSnapshot, updatePassedSnapshot })) { + mkdirp.sync(snapshotsDir) + fs.writeFileSync(baselineSnapshotPath, receivedImageBuffer) + result = { updated: true } + } else { + result = { + pass, + diffRatio, + diffPixelCount, + diffOutputPath, + } + } + } + return result +} + +export const runDiffImageToSnapshot = options => { + options.receivedImageBuffer = options.receivedImageBuffer.toString('base64') + + const serializedInput = JSON.stringify(options) + + let result = {} + + const writeDiffProcess = childProcess.spawnSync( + process.execPath, + [`${__dirname}/diff-process.js`], + { + input: Buffer.from(serializedInput), + stdio: ['pipe', 'inherit', 'inherit', 'pipe'], + }, + ) + + if (writeDiffProcess.status === 0) { + const output = writeDiffProcess.output[3].toString() + result = JSON.parse(output) + } else { + throw new Error('Error running image diff.') + } + + return result +} diff --git a/src/jest/image-composer.js b/src/jest/image-composer.js new file mode 100644 index 0000000..123f635 --- /dev/null +++ b/src/jest/image-composer.js @@ -0,0 +1,63 @@ +const getMaxImageSize = images => { + let maxWidth = 0 + let maxHeight = 0 + + images.forEach(image => { + if (image.imageWidth > maxWidth) { + maxWidth = image.imageWidth + } + + if (image.imageHeight > maxHeight) { + maxHeight = image.imageHeight + } + }) + + return { + maxWidth, + maxHeight, + } +} + +const ImageComposer = function ImageComposer(options = {}) { + this.direction = options.direction || 'horizontal' + this.images = [] + + return this +} + +ImageComposer.prototype.addImage = function addImage( + imageData, + imageWidth, + imageHeight, +) { + this.images.push({ + imageData, + imageWidth, + imageHeight, + }) + + return this +} + +ImageComposer.prototype.getParams = function getParams() { + const { maxWidth, maxHeight } = getMaxImageSize(this.images) + + const compositeWidth = + maxWidth * (this.direction === 'horizontal' ? this.images.length : 1) + const compositeHeight = + maxHeight * (this.direction === 'vertical' ? this.images.length : 1) + const offsetX = this.direction === 'horizontal' ? maxWidth : 0 + const offsetY = this.direction === 'vertical' ? maxHeight : 0 + + return { + direction: this.direction, + images: this.images, + imagesCount: this.images.length, + compositeWidth, + compositeHeight, + offsetX, + offsetY, + } +} + +module.exports = ImageComposer diff --git a/src/jest/imageExpect.ts b/src/jest/imageExpect.ts deleted file mode 100644 index 338649e..0000000 --- a/src/jest/imageExpect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { configureToMatchImageSnapshot } from 'jest-image-snapshot' - -export const toMatchImageSnapshot = (customConfig = {}) => { - return configureToMatchImageSnapshot(customConfig) -} diff --git a/src/jest/index.js b/src/jest/index.js new file mode 100644 index 0000000..1480229 --- /dev/null +++ b/src/jest/index.js @@ -0,0 +1,244 @@ +/* eslint-disable no-underscore-dangle */ +import kebabCase from 'lodash/kebabCase' +import merge from 'lodash/merge' +import path from 'path' +import fs from 'fs' +import { diffImageToSnapshot, runDiffImageToSnapshot } from './diff-snapshot' +import { stopServerAndBrowser } from '../server' +import { imageThumbnail } from '../server/utils/error' + +const Chalk = require('chalk').constructor + +const timesCalled = new Map() + +const SNAPSHOTS_DIR = '__image_snapshots__' + +function updateSnapshotState(originalSnapshotState, partialSnapshotState) { + if (global.UNSTABLE_SKIP_REPORTING) { + return originalSnapshotState + } + return merge(originalSnapshotState, partialSnapshotState) +} + +async function checkResult({ + result, + snapshotState, + retryTimes, + snapshotIdentifier, + chalk, +}) { + let pass = true + /* + istanbul ignore next + `message` is implementation detail. Actual behavior is tested in integration.spec.js + */ + let message = () => '' + + if (result.updated) { + // once transition away from jasmine is done this will be a lot more elegant and pure + // https://github.com/facebook/jest/pull/3668 + updateSnapshotState(snapshotState, { updated: snapshotState.updated + 1 }) + } else if (result.added) { + updateSnapshotState(snapshotState, { added: snapshotState.added + 1 }) + } else { + ;({ pass } = result) + + updateSnapshotState(snapshotState, { matched: snapshotState.matched + 1 }) + + if (!pass) { + // Stop server + await stopServerAndBrowser() + const currentRun = timesCalled.get(snapshotIdentifier) + if (!retryTimes || currentRun > retryTimes) { + updateSnapshotState(snapshotState, { + unmatched: snapshotState.unmatched + 1, + }) + } + + const differencePercentage = result.diffRatio * 100 + message = () => { + let failure + if (result.diffSize) { + failure = + `Expected image to be the same size as the snapshot (${result.imageDimensions.baselineWidth}x${result.imageDimensions.baselineHeight}), but was different (${result.imageDimensions.receivedWidth}x${result.imageDimensions.receivedHeight}).\n` + + `${chalk.bold.red('See diff for details:')} ${chalk.red( + result.diffOutputPath, + )}` + + `${imageThumbnail(result.diffOutputPath, '700px')}` + } else { + failure = + `Expected image to match or be a close match to snapshot but was ${differencePercentage}% different from snapshot (${result.diffPixelCount} differing pixels).\n` + + `${chalk.bold.red('See diff for details:')} ${chalk.red( + result.diffOutputPath, + )}` + + `${imageThumbnail(result.diffOutputPath, '700px')}` + } + + return failure + } + } + } + + return { + message, + pass, + } +} + +function createSnapshotIdentifier({ + retryTimes, + testPath, + currentTestName, + customSnapshotIdentifier, + snapshotState, +}) { + const counter = snapshotState._counters.get(currentTestName) + const defaultIdentifier = kebabCase( + `${path.basename(testPath)}-${currentTestName}-${counter}`, + ) + + let snapshotIdentifier = customSnapshotIdentifier || defaultIdentifier + + if (typeof customSnapshotIdentifier === 'function') { + const customRes = customSnapshotIdentifier({ + testPath, + currentTestName, + counter, + defaultIdentifier, + }) + + if (retryTimes && !customRes) { + throw new Error( + 'A unique customSnapshotIdentifier must be set when jest.retryTimes() is used', + ) + } + + snapshotIdentifier = customRes || defaultIdentifier + } + + if (retryTimes) { + if (!customSnapshotIdentifier) + throw new Error( + 'A unique customSnapshotIdentifier must be set when jest.retryTimes() is used', + ) + + timesCalled.set( + snapshotIdentifier, + (timesCalled.get(snapshotIdentifier) || 0) + 1, + ) + } + + return snapshotIdentifier +} + +function configureToMatchImageSnapshot({ + customDiffConfig: commonCustomDiffConfig = {}, + customSnapshotsDir: commonCustomSnapshotsDir, + customDiffDir: commonCustomDiffDir, + diffDirection: commonDiffDirection = 'horizontal', + noColors: commonNoColors = false, + failureThreshold: commonFailureThreshold = 0, + failureThresholdType: commonFailureThresholdType = 'pixel', + updatePassedSnapshot: commonUpdatePassedSnapshot = false, + runInProcess: commonRunInProcess = false, +} = {}) { + return function toMatchImageSnapshot( + received, + { + customSnapshotIdentifier = '', + customSnapshotsDir = commonCustomSnapshotsDir, + customDiffDir = commonCustomDiffDir, + diffDirection = commonDiffDirection, + customDiffConfig = {}, + noColors = commonNoColors, + failureThreshold = commonFailureThreshold, + failureThresholdType = commonFailureThresholdType, + updatePassedSnapshot = commonUpdatePassedSnapshot, + runInProcess = commonRunInProcess, + } = {}, + ) { + const { testPath, currentTestName, isNot, snapshotState } = this + const chalk = new Chalk({ enabled: !noColors }) + + const retryTimes = parseInt(global[Symbol.for('RETRY_TIMES')], 10) || 0 + + if (isNot) { + throw new Error( + 'Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.', + ) + } + + updateSnapshotState(snapshotState, { + _counters: snapshotState._counters.set( + currentTestName, + (snapshotState._counters.get(currentTestName) || 0) + 1, + ), + }) // eslint-disable-line max-len + + const snapshotIdentifier = createSnapshotIdentifier({ + retryTimes, + testPath, + currentTestName, + customSnapshotIdentifier, + snapshotState, + }) + + const snapshotsDir = + customSnapshotsDir || path.join(path.dirname(testPath), SNAPSHOTS_DIR) + const diffDir = customDiffDir || path.join(snapshotsDir, '__diff_output__') + const baselineSnapshotPath = path.join( + snapshotsDir, + `${snapshotIdentifier}-snap.png`, + ) + + if ( + snapshotState._updateSnapshot === 'none' && + !fs.existsSync(baselineSnapshotPath) + ) { + return { + pass: false, + message: () => + `New snapshot was ${chalk.bold.red( + 'not written', + )}. The update flag must be explicitly ` + + 'passed to write a new snapshot.\n\n + This is likely because this test is run in a continuous ' + + 'integration (CI) environment in which snapshots are not written by default.\n\n', + } + } + + const imageToSnapshot = runInProcess + ? diffImageToSnapshot + : runDiffImageToSnapshot + + const result = imageToSnapshot({ + receivedImageBuffer: received, + snapshotsDir, + diffDir, + diffDirection, + snapshotIdentifier, + updateSnapshot: snapshotState._updateSnapshot === 'all', + customDiffConfig: Object.assign( + {}, + commonCustomDiffConfig, + customDiffConfig, + ), + failureThreshold, + failureThresholdType, + updatePassedSnapshot, + }) + + return checkResult({ + result, + snapshotState, + retryTimes, + snapshotIdentifier, + chalk, + }) + } +} + +module.exports = { + toMatchImageSnapshot: configureToMatchImageSnapshot(), + configureToMatchImageSnapshot, + updateSnapshotState, +} diff --git a/src/server/index.ts b/src/server/index.ts index 04fc860..ad47f96 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -72,12 +72,19 @@ export const initializeSparkTestBrowser = (testOptions: TestOptions = {}) => { // Screenshot handling => /upload expressApp.post('/upload', (req, res) => { - const { pngImage, imagePathName } = req.body + const { pngImage, imagePathName, ticketId } = req.body const imageBuffer = decodeBase64Image(pngImage) - fs.writeFile(imagePathName, imageBuffer.data, err => { - if (err) res.status(400).send('Error saving image') - res.status(200).send() - }) + if (imagePathName) { + fs.writeFile(imagePathName, imageBuffer.data, err => { + if (err) res.status(400).send('Error saving image') + res.status(200).send() + }) + } + // Handle ticketId callback + const callbackQueueEvent = websocketMessageQueue.get(ticketId) + if (!callbackQueueEvent) + throw new Error('Unknown message received from client') + callbackQueueEvent.resolve(imageBuffer.data) }) // Serve spark application @@ -201,7 +208,7 @@ export const refreshSparkBrowser = async (sparkApplicationPath: string) => { }) } -export const takeScreenshot = (path: string) => +export const takeScreenshot = (path?: string) => sendInfoToClients({ action: SparkBrowserActions.TAKE_SCREENSHOT, payload: path, diff --git a/src/server/utils/error.ts b/src/server/utils/error.ts index 1b661ea..70b97ab 100644 --- a/src/server/utils/error.ts +++ b/src/server/utils/error.ts @@ -1,8 +1,8 @@ import { takeScreenshot, stopServerAndBrowser } from '../' -const imageThumbnail = (path: string) => (` +export const imageThumbnail = (path: string, width: string = '300px') => (` - + `) From c6d4d79b5f446b379d8135ee8442128b56ea2dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 15:43:53 +0200 Subject: [PATCH 05/16] fix hello world test --- test/local/helloworld.spark.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/local/helloworld.spark.test.js b/test/local/helloworld.spark.test.js index 52ee0b0..d0fea53 100644 --- a/test/local/helloworld.spark.test.js +++ b/test/local/helloworld.spark.test.js @@ -24,6 +24,5 @@ test('Should be able to assert on element', async done => { const element = await findElementWithPropertyValue('text', 'Hello World!') expect(element).toBeTruthy() expect(element.id).toBe('my-test-id') - const element1 = await findElementWithPropertyValue('text', 'findes ikke') done() }) From 687737bd93f9a845bc871a693a475096fdaf8301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 15:53:38 +0200 Subject: [PATCH 06/16] add snapshot test case --- ...ld-be-able-to-assert-on-element-1-snap.png | Bin 0 -> 4354 bytes ...ld-be-able-to-assert-on-element-2-snap.png | Bin 0 -> 4353 bytes test/local/snapshots.spark.test.js | 40 ++++++++++++++++++ test/local/sparkApplications/box.js | 20 +++++++++ 4 files changed, 60 insertions(+) create mode 100644 test/local/__image_snapshots__/snapshots-spark-test-js-should-be-able-to-assert-on-element-1-snap.png create mode 100644 test/local/__image_snapshots__/snapshots-spark-test-js-should-be-able-to-assert-on-element-2-snap.png create mode 100644 test/local/snapshots.spark.test.js create mode 100644 test/local/sparkApplications/box.js diff --git a/test/local/__image_snapshots__/snapshots-spark-test-js-should-be-able-to-assert-on-element-1-snap.png b/test/local/__image_snapshots__/snapshots-spark-test-js-should-be-able-to-assert-on-element-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..eee454c88383d20850413c60b176c58c0e3ca5c2 GIT binary patch literal 4354 zcmeAS@N?(olHy`uVBq!ia0y~yUe5sRjdP0g&h+ z9tRM`VLU218VI8)VKh4c)4*sTj0VDJ?J!zJQr1oYRVJfgGz3ONU^E0qLtr!nMnhmU d1TaFN?ki)QAWMXLdV3y7&ePS;Wt~$(69CuZ0bc+B literal 0 HcmV?d00001 diff --git a/test/local/__image_snapshots__/snapshots-spark-test-js-should-be-able-to-assert-on-element-2-snap.png b/test/local/__image_snapshots__/snapshots-spark-test-js-should-be-able-to-assert-on-element-2-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..f17f9c0e4ad26e8aaf421c97718a8169607011ba GIT binary patch literal 4353 zcmeAS@N?(olHy`uVBq!ia0y~yU~2RNipCW;-*GW@sqpaRAXA#te)=O5qFx3y^9s zU={$09vYP#4TRB@Fq$1k%Y@N_akO?Ats*IJCxDt2qhK@yMnhmU1V%$(Gz3ONU^E2K cL!e=C-KKh$2-^);|AM4EUHx3vIVCg!0OS4xxBvhE literal 0 HcmV?d00001 diff --git a/test/local/snapshots.spark.test.js b/test/local/snapshots.spark.test.js new file mode 100644 index 0000000..3ec3bf4 --- /dev/null +++ b/test/local/snapshots.spark.test.js @@ -0,0 +1,40 @@ +import path from 'path' +import { getTestOptions } from './utils/testOptions' +import { + initializeSparkTestBrowser, + refreshSparkBrowser, + findElementWithPropertyValue, + stopServerAndBrowser, + takeScreenshot, + sendKeyEvent, +} from '../../dist/index' + +beforeEach(async done => { + await initializeSparkTestBrowser(getTestOptions()) + done() +}) + +afterEach(async done => { + await stopServerAndBrowser() + done() +}) + +test('Should be able to assert on element', async done => { + await refreshSparkBrowser( + path.resolve(__dirname, './sparkApplications/box.js'), + ) + const element = await findElementWithPropertyValue('id', 'my-rect') + expect(element).toBeTruthy() + + // Screenshot match testing + const firstScreenshot = await takeScreenshot() + expect(firstScreenshot).toMatchImageSnapshot() + + // Key events will change fillColor of rect + await sendKeyEvent('onKeyDown', 'red') + // Screenshot match testing + const secondScreenshot = await takeScreenshot() + expect(secondScreenshot).toMatchImageSnapshot() + + done() +}) diff --git a/test/local/sparkApplications/box.js b/test/local/sparkApplications/box.js new file mode 100644 index 0000000..8a2f607 --- /dev/null +++ b/test/local/sparkApplications/box.js @@ -0,0 +1,20 @@ +/* eslint-disable */ + +// Create colored box +px.import('px:scene.1.js').then(scene => { + let box = scene.create({ + t: 'rect', + id: 'my-rect', + parent: scene.root, + fillColor: 'green', + w: 300, + h: 300, + }) + + scene.root.on('onKeyDown', function(e) { + // Change color to red + box.fillColor = e.keyCode + }) + + scene.on('onClose', () => console.log('Hello got OnClose')) +}) From dc75c2cf2e7fb654e0dacd8c6ee1a2f8bc8f671f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 15:56:20 +0200 Subject: [PATCH 07/16] test failed snapshot on ci --- test/local/snapshots.spark.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/local/snapshots.spark.test.js b/test/local/snapshots.spark.test.js index 3ec3bf4..f0b5d23 100644 --- a/test/local/snapshots.spark.test.js +++ b/test/local/snapshots.spark.test.js @@ -31,7 +31,7 @@ test('Should be able to assert on element', async done => { expect(firstScreenshot).toMatchImageSnapshot() // Key events will change fillColor of rect - await sendKeyEvent('onKeyDown', 'red') + await sendKeyEvent('onKeyDown', 'blue') // Screenshot match testing const secondScreenshot = await takeScreenshot() expect(secondScreenshot).toMatchImageSnapshot() From 83d78bc29834e481ff9db29feaf58ab5bbd34543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 15:58:52 +0200 Subject: [PATCH 08/16] Revert "test failed snapshot on ci" This reverts commit dc75c2cf2e7fb654e0dacd8c6ee1a2f8bc8f671f. --- test/local/snapshots.spark.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/local/snapshots.spark.test.js b/test/local/snapshots.spark.test.js index f0b5d23..3ec3bf4 100644 --- a/test/local/snapshots.spark.test.js +++ b/test/local/snapshots.spark.test.js @@ -31,7 +31,7 @@ test('Should be able to assert on element', async done => { expect(firstScreenshot).toMatchImageSnapshot() // Key events will change fillColor of rect - await sendKeyEvent('onKeyDown', 'blue') + await sendKeyEvent('onKeyDown', 'red') // Screenshot match testing const secondScreenshot = await takeScreenshot() expect(secondScreenshot).toMatchImageSnapshot() From 2ee9c6f86d36d8aa2c95bb552f0407569d72a24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:06:40 +0200 Subject: [PATCH 09/16] image snapshot testing fixes --- .gitignore | 1 + jest.config.js | 11 ++++++++++- jest.setup.js | 6 +++++- package-lock.json | 9 +++++++++ package.json | 1 + src/index.ts | 4 +++- src/server/index.ts | 1 + src/server/utils/error.ts | 23 ++++++++++++++--------- 8 files changed, 44 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 9b615cd..d963993 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ npm-debug.log* /local /reports /node_modules +/results .DS_Store Thumbs.db diff --git a/jest.config.js b/jest.config.js index 9147e1f..8d2ee61 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,13 @@ module.exports = { setupFilesAfterEnv: ['./jest.setup.js'], - reporters: ['default', 'jest-html-reporters'], + reporters: [ + 'default', + [ + 'jest-html-reporters', + { + publicPath: './results', + filename: 'report.html', + }, + ], + ], } diff --git a/jest.setup.js b/jest.setup.js index bbd00a4..f84d865 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,7 +1,11 @@ -import { toMatchImageSnapshot } from './dist' +import { configureToMatchImageSnapshot } from './dist' jest.setTimeout(30000) +const toMatchImageSnapshot = configureToMatchImageSnapshot({ + customDiffDir: './results', +}) + expect.extend({ toMatchImageSnapshot, }) diff --git a/package-lock.json b/package-lock.json index 5e55cb5..70687cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1272,6 +1272,15 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, + "@types/mkdirp": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", + "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "12.6.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.6.tgz", diff --git a/package.json b/package.json index 4c4d20c..aa6671c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@babel/preset-typescript": "^7.3.3", "@types/express": "^4.17.0", "@types/express-http-proxy": "^1.5.11", + "@types/mkdirp": "^0.5.2", "@types/node": "^12.6.6", "@types/request-promise": "^4.1.44", "@types/uid2": "0.0.0", diff --git a/src/index.ts b/src/index.ts index 77e0232..4909d21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,8 @@ import { sendKeyEvent, } from './server' import { - toMatchImageSnapshot + toMatchImageSnapshot, + configureToMatchImageSnapshot, } from './jest' module.exports = { @@ -22,4 +23,5 @@ module.exports = { findElementsWithPropertyValue, sendKeyEvent, toMatchImageSnapshot, + configureToMatchImageSnapshot, } diff --git a/src/server/index.ts b/src/server/index.ts index ad47f96..1e978b1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -16,6 +16,7 @@ import { transformSparkCode, } from '../babel/transformSparkApplication' +export const publicPath = `${process.cwd()}/results` let websocketServer: WebSocket.Server = null let sparkApplicationPath: string = '/Applications/Spark.app/Contents/MacOS/spark.sh' let expressApp:Express = null diff --git a/src/server/utils/error.ts b/src/server/utils/error.ts index 70b97ab..3e638f4 100644 --- a/src/server/utils/error.ts +++ b/src/server/utils/error.ts @@ -1,15 +1,19 @@ -import { takeScreenshot, stopServerAndBrowser } from '../' +import fs from 'fs' +import path from 'path' +import uid from 'uid2' +import mkdirp from 'mkdirp' +import { takeScreenshot, stopServerAndBrowser, publicPath } from '../' -export const imageThumbnail = (path: string, width: string = '300px') => (` - - +export const imageThumbnail = (filePath: string, width: string = '300px') => (` + + `) -const screenshotError = (path: string, error: Error) => { +const screenshotError = (filePath: string, error: Error) => { error.stack = ` ${error.stack} - ${imageThumbnail(path)} + ${imageThumbnail(filePath)} ` return error } @@ -21,7 +25,10 @@ export const killProcessAndThrowError = async (error: Error) => { export const throwErrorWithScreenshot = async (error: Error) => { // Try to take screenshot if possible - const screenshotPath = `${process.cwd()}/test.png` + const screenshotPath = `${publicPath}/${uid(10)}.png` + if (!fs.existsSync(path.dirname(publicPath))) { + mkdirp.sync(publicPath) + } return new Promise(resolve => { takeScreenshot(screenshotPath) .then(async () => { @@ -29,6 +36,4 @@ export const throwErrorWithScreenshot = async (error: Error) => { resolve() }) }) - - return } \ No newline at end of file From 11054358aacc25f1750068ad01ac78d88625b168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:21:51 +0200 Subject: [PATCH 10/16] store test results --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 96fe933..a3d2cfb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,6 +28,8 @@ jobs: command: npm run test environment: PXSCENE_PATH: "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x16' /usr/src/app/pxCore/examples/pxScene2d/src/spark.sh" + - store_test_data: + to: 'results' workflows: version: 2 test: From e5acbaf38914a6e28ff03065ffcd1c8f0ba23a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:22:11 +0200 Subject: [PATCH 11/16] test report on circle ci --- test/local/helloworld.spark.test.js | 1 + test/local/snapshots.spark.test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/local/helloworld.spark.test.js b/test/local/helloworld.spark.test.js index d0fea53..15fa6ff 100644 --- a/test/local/helloworld.spark.test.js +++ b/test/local/helloworld.spark.test.js @@ -24,5 +24,6 @@ test('Should be able to assert on element', async done => { const element = await findElementWithPropertyValue('text', 'Hello World!') expect(element).toBeTruthy() expect(element.id).toBe('my-test-id') + const element1 = await findElementWithPropertyValue('blaa', 'no way') done() }) diff --git a/test/local/snapshots.spark.test.js b/test/local/snapshots.spark.test.js index 3ec3bf4..f0b5d23 100644 --- a/test/local/snapshots.spark.test.js +++ b/test/local/snapshots.spark.test.js @@ -31,7 +31,7 @@ test('Should be able to assert on element', async done => { expect(firstScreenshot).toMatchImageSnapshot() // Key events will change fillColor of rect - await sendKeyEvent('onKeyDown', 'red') + await sendKeyEvent('onKeyDown', 'blue') // Screenshot match testing const secondScreenshot = await takeScreenshot() expect(secondScreenshot).toMatchImageSnapshot() From b1c9a54bda3c07b0c05ab2738d8f2cef57e54032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:24:51 +0200 Subject: [PATCH 12/16] fix ci config --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a3d2cfb..4b989c3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,8 +28,8 @@ jobs: command: npm run test environment: PXSCENE_PATH: "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x16' /usr/src/app/pxCore/examples/pxScene2d/src/spark.sh" - - store_test_data: - to: 'results' + - store_artifacts: + path: ~/spark/results workflows: version: 2 test: From 5f1a2ef79b81699845936c56f163337b613a08c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:38:02 +0200 Subject: [PATCH 13/16] add global teardown method --- globalTeardown.js | 3 +++ jest.config.js | 1 + 2 files changed, 4 insertions(+) create mode 100644 globalTeardown.js diff --git a/globalTeardown.js b/globalTeardown.js new file mode 100644 index 0000000..bb0a480 --- /dev/null +++ b/globalTeardown.js @@ -0,0 +1,3 @@ +import { stopServerAndBrowser } from './dist/index' + +module.exports = stopServerAndBrowser diff --git a/jest.config.js b/jest.config.js index 8d2ee61..c14a502 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,6 @@ module.exports = { setupFilesAfterEnv: ['./jest.setup.js'], + globalTeardown: './globalTeardown.js', reporters: [ 'default', [ From 083bbf8b57a3c2438c596228fbe27ddbde5c81e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:41:45 +0200 Subject: [PATCH 14/16] force exit on circle ci --- .circleci/config.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b989c3..212db43 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,7 +25,7 @@ jobs: command: npm run build - run: name: test - command: npm run test + command: npm run test:circleCI environment: PXSCENE_PATH: "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x16' /usr/src/app/pxCore/examples/pxScene2d/src/spark.sh" - store_artifacts: diff --git a/package.json b/package.json index aa6671c..701c0ca 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "security": "npm audit", "test": "jest './test/local/.*.spark.test.js' --runInBand", "test:remote": "jest './test/remote/.*.spark.test.js' --runInBand", - "test:circleCI": "jest './test/local/.*.spark.test.js' --runInBand", + "test:circleCI": "jest './test/local/.*.spark.test.js' --runInBand --forceExit", "pretest": "npm run lint" }, "keywords": [ From c49e211f71e490bb44c89ab3f8c6ebba02667379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:41:55 +0200 Subject: [PATCH 15/16] Revert "add global teardown method" This reverts commit 5f1a2ef79b81699845936c56f163337b613a08c3. --- globalTeardown.js | 3 --- jest.config.js | 1 - 2 files changed, 4 deletions(-) delete mode 100644 globalTeardown.js diff --git a/globalTeardown.js b/globalTeardown.js deleted file mode 100644 index bb0a480..0000000 --- a/globalTeardown.js +++ /dev/null @@ -1,3 +0,0 @@ -import { stopServerAndBrowser } from './dist/index' - -module.exports = stopServerAndBrowser diff --git a/jest.config.js b/jest.config.js index c14a502..8d2ee61 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,5 @@ module.exports = { setupFilesAfterEnv: ['./jest.setup.js'], - globalTeardown: './globalTeardown.js', reporters: [ 'default', [ From 67d28a5950e141bbab694576e7acc831c263b6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gustafson?= Date: Wed, 28 Aug 2019 17:46:05 +0200 Subject: [PATCH 16/16] fix tests --- test/local/helloworld.spark.test.js | 1 - test/local/snapshots.spark.test.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/local/helloworld.spark.test.js b/test/local/helloworld.spark.test.js index 15fa6ff..d0fea53 100644 --- a/test/local/helloworld.spark.test.js +++ b/test/local/helloworld.spark.test.js @@ -24,6 +24,5 @@ test('Should be able to assert on element', async done => { const element = await findElementWithPropertyValue('text', 'Hello World!') expect(element).toBeTruthy() expect(element.id).toBe('my-test-id') - const element1 = await findElementWithPropertyValue('blaa', 'no way') done() }) diff --git a/test/local/snapshots.spark.test.js b/test/local/snapshots.spark.test.js index f0b5d23..3ec3bf4 100644 --- a/test/local/snapshots.spark.test.js +++ b/test/local/snapshots.spark.test.js @@ -31,7 +31,7 @@ test('Should be able to assert on element', async done => { expect(firstScreenshot).toMatchImageSnapshot() // Key events will change fillColor of rect - await sendKeyEvent('onKeyDown', 'blue') + await sendKeyEvent('onKeyDown', 'red') // Screenshot match testing const secondScreenshot = await takeScreenshot() expect(secondScreenshot).toMatchImageSnapshot()