From cc1997cc502acce164c9e810b11058e2185717af Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 2 Dec 2023 13:20:16 -0500 Subject: [PATCH 1/2] Create visual test system --- contributor_docs/unit_testing.md | 21 ++ tasks/test/mocha-chrome.js | 23 ++ test/unit/spec.js | 8 + test/unit/visual/cases/webgl.js | 37 ++++ .../2D objects maintain correct size/000.png | Bin 0 -> 265 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 219 bytes .../001.png | Bin 0 -> 209 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 1205 bytes .../metadata.json | 3 + test/unit/visual/visualTest.js | 196 ++++++++++++++++++ test/visual.html | 16 ++ test/visual/style.css | 45 ++++ test/visual/visualTestList.js | 8 + test/visual/visualTestRunner.js | 125 +++++++++++ 16 files changed, 488 insertions(+) create mode 100644 test/unit/visual/cases/webgl.js create mode 100644 test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/000.png create mode 100644 test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/000.png create mode 100644 test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/001.png create mode 100644 test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/000.png create mode 100644 test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/metadata.json create mode 100644 test/unit/visual/visualTest.js create mode 100644 test/visual.html create mode 100644 test/visual/style.css create mode 100644 test/visual/visualTestList.js create mode 100644 test/visual/visualTestRunner.js diff --git a/contributor_docs/unit_testing.md b/contributor_docs/unit_testing.md index f0f4f76cab..64ef0f73b9 100644 --- a/contributor_docs/unit_testing.md +++ b/contributor_docs/unit_testing.md @@ -118,3 +118,24 @@ test('keyIsPressed is a boolean', function() { Similarly we can use `assert.strictEqual(myp5.keyIsPressed, true)` to assert if the value is true. You can read more about chai's assert [here](https://www.chaijs.com/api/assert/) Now that you have written the tests, run them and see if the method behaves as expected. If not, create an issue for the same and if you want, you can even work on fixing it! + +## Visual tests + +Visual tests are a way to make sure sketches do not unexpectedly change when we change the implementation of p5.js features. Each visual test file lives in the `test/unit/visual/cases` folder. Inside each file there are multiple visual test cases. Each case creates a sample sketch, and then calls `screenshot()` to check how the sketch looks. + +```js +visualTest('2D objects maintain correct size', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.noStroke(); + p5.fill('red'); + p5.rectMode(p5.CENTER); + p5.rect(0, 0, p5.width/2, p5.height/2); + screenshot(); +}); +``` + +If you need to add a new test file, add it to that folder, then add the filename to the list in `test/visual/visualTestList.js`. Additionally, if you want that file to be run automatically as part of continuous integration on every pull request, add the filename to the `visual` list in `test/unit/spec.js`. + +When you add a new test, running `npm test` will generate new screenshots for any visual tests that do not yet have them. Those screenshots will then be used as a reference the next time tests run to make sure the sketch looks the same. If a test intentionally needs to look different, you can delete the folder matching the test name in the `test/unit/visual/screenshots` folder, and then re-run `npm test` to generate a new one. + +To manually inspect all visual tests, run `grunt yui:dev` to launch a local server, then go to http://127.0.0.1:9001/test/visual.html to see a list of all test cases. diff --git a/tasks/test/mocha-chrome.js b/tasks/test/mocha-chrome.js index 2fccc5eda7..d2d998fe86 100644 --- a/tasks/test/mocha-chrome.js +++ b/tasks/test/mocha-chrome.js @@ -4,6 +4,7 @@ const puppeteer = require('puppeteer'); const util = require('util'); const mapSeries = require('promise-map-series'); const fs = require('fs'); +const path = require('path'); const EventEmitter = require('events'); const mkdir = util.promisify(fs.mkdir); @@ -28,6 +29,28 @@ module.exports = function(grunt) { const page = await browser.newPage(); try { + // Set up visual tests + await page.evaluateOnNewDocument(function(shouldGenerateScreenshots) { + window.shouldGenerateScreenshots = shouldGenerateScreenshots; + }, !process.env.CI); + + await page.exposeFunction('writeImageFile', function(filename, base64Data) { + fs.mkdirSync('test/' + path.dirname(filename), { recursive: true }); + const prefix = /^data:image\/\w+;base64,/; + fs.writeFileSync( + 'test/' + filename, + base64Data.replace(prefix, ''), + 'base64' + ); + }); + await page.exposeFunction('writeFile', function(filename, data) { + fs.mkdirSync('test/' + path.dirname(filename), { recursive: true }); + fs.writeFileSync( + 'test/' + filename, + data + ); + }); + // Using eval to start the test in the browser // A 'mocha:end' event will be triggered with test runner end await page.evaluateOnNewDocument(` diff --git a/test/unit/spec.js b/test/unit/spec.js index 6d4c8f4142..64c8cc8f91 100644 --- a/test/unit/spec.js +++ b/test/unit/spec.js @@ -47,8 +47,16 @@ var spec = { 'p5.Shader', 'p5.Texture', 'light' + ], + 'visual/cases': [ + // Add the visual tests that you want run as part of CI here. Feel free + // to omit some for speed if they should only be run manually. + 'webgl' ] }; +document.write( + '' +); Object.keys(spec).map(function(folder) { spec[folder].map(function(file) { var string = [ diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js new file mode 100644 index 0000000000..c45ebda9a5 --- /dev/null +++ b/test/unit/visual/cases/webgl.js @@ -0,0 +1,37 @@ +visualSuite('WebGL', function() { + visualSuite('Camera', function() { + visualTest('2D objects maintain correct size', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.noStroke(); + p5.fill('red'); + p5.rectMode(p5.CENTER); + p5.rect(0, 0, p5.width/2, p5.height/2); + screenshot(); + }); + + visualTest('Custom camera before and after resize', function(p5, screenshot) { + p5.createCanvas(25, 50, p5.WEBGL); + const cam = p5.createCamera(); + p5.setCamera(cam); + cam.setPosition(-10, -10, 800); + p5.strokeWeight(4); + p5.box(20); + screenshot(); + + p5.resizeCanvas(50, 25); + p5.box(20); + screenshot(); + }); + }); + + visualSuite('Lights', function() { + visualTest('Fill color and default ambient material', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.noStroke(); + p5.lights(); + p5.fill('red'); + p5.sphere(20); + screenshot(); + }); + }); +}); diff --git a/test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/000.png b/test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/000.png new file mode 100644 index 0000000000000000000000000000000000000000..d2daba574f6bb66d7f89a023dc24e0301dea0521 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}hdo^!Lo%G- z&bY|iY#`t~_v|jow`n*_$scaFX<~9zql!I<5nY}$m65eHdPAhN{dEAB)zv?lM^qIyixk< zM^oP2<*9QeR32Xqf9x^8@EW7LQCzKQ-xtYr*#eMYJ!9)9KjRSd2Pc5uVeoYIb6Mw< G&;$U=zifm6 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/metadata.json b/test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/Camera/2D objects maintain correct size/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/000.png b/test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/000.png new file mode 100644 index 0000000000000000000000000000000000000000..b3cc6ed6097a9f86ddf39cef8be973a9de4915a8 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^l0a<4!3HE(HWf?*QjEnx?oJHr&dIz4a;AE^IEF;H zzny-X_kaSA^Vzfv`-;fy^k@qtd-oN}8u7v;npYvoYbxcc-C z0Y;UwQb}>8h9g^c`I-FIy?1VjcUHaor2I?O;n~@Z6X(<@Z9KWApojaff6wV1D)QRD zWwp0nySeDzzYn#W-Y6`rHJVm8;iSB7XG)J}r4aApuPH3|V+Er$)D@czUoeG9dAycj SIeQi8Kn71&KbLh*2~7ZnFjN%) literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/001.png b/test/unit/visual/screenshots/WebGL/Camera/Custom camera before and after resize/001.png new file mode 100644 index 0000000000000000000000000000000000000000..74fb513043820a27027baced6e58dcec97e53d13 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^MnEjd!3HFYLuy@s6k~CayA#8@b22Z1oNiAS$B+p3 zx03_84k++AKR)~BtW8w@vl|k82m8x{%vJvV5MFxHTiDDpQ&@h%#QhUh1cf-g8m@A2 zefzpybFV&m$u&y{C8ki< zb3b=6Ug#(^owdfPx(Z%IT!RA@u(ncHp?R}_XnTH2ID2q_dPf_K4rnO?Mjnv1AHRpc2VKqwa+)N;Yg z*t`n}sX%Cwo;Vu)W>#5R+Iu`Rc2dhqq%E9a&CECd`qyFaV=Tzeu?6Aa2xQ)rJW?d5 z$W16Nz~z5o9JgOWPv3nH!x8Wncn#%ipn&15l%0tpx1l%z{ZpSFe7wG>K{90{I7c-U6l(h_cc-=%02FGPeiuA(aEs!<$c1#utdpZ+`g`c-atU zB1j3vMd+I+>ePr~RTo+j#KOx|@yayGXcIwxh0!wLL&_W6d!s9eWiW4iQ{``9AGq>4 z0VWGhEl2^yr7EOkZd1d2k*XW95T-n0YUIL8Kn5beD8!yXsprX+&03I$2{nA-6Npc_ zb&8ljQkly#m{oi$lu?x*YE;RB$Qxb_Q^dl`7e9U0g}Q`R%wFadQ^nG;snXA=k_EX3 zgH`CMe~MuhQ*JDTx@hjgb&B{@9K-7`U=M~@VLcUI$@^_XZy5&On(@b#ULd=fcBNBjrBZKH z#ZQNIS<4qh8?<6kv<6ZjtwJkj_DSj2hgIE#m@A5Pq1DOkbDn;OTTRVwGlQlDjW$UL+tnoIU!eX|Y7eHfhu=Ba6X1;d}(G>ytKP22cfK}>bP z-sFC`lF4JQHM2HeFw+iJF$JeUeB*c^gOwg)VMblSEX3x6K(x=h_Wy4OB~z}S6en&6 z>B|eTL1)y0(GI*Xkab(8sTL)*)0o;awbAw!l$#Cre;X+hVKi)Y-NddtTCh%6v&SBk z8x0{+4Qg)x>%YJ2Xze`o4QAHI3Ti{N-+Q3P3Pxivapy6PzWz=54LV=^{6UYD&<=c; zldD#t73H#wri)B-^d^9PD1UAdO7U5y0`Us1z~v?AO*#zqYP_uuL)#qablUr!XqfPY z0(wdUOVF1>dQ0(m`VBnRkkr-#lb&TJicGr6Vf_CQ$YH$YHupGEq|MtpjPHH~4mqDu T=tMP600000NkvXXu0mjfUOzEb literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/metadata.json b/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js new file mode 100644 index 0000000000..8c37829f14 --- /dev/null +++ b/test/unit/visual/visualTest.js @@ -0,0 +1,196 @@ +/** + * A helper class to contain an error and also the screenshot data that + * caused the error. + */ +class ScreenshotError extends Error { + constructor(message, actual, expected) { + super(message); + this.actual = actual; + this.expected = expected; + } +} + +function toBase64(img) { + return img.canvas.toDataURL(); +} + +function escapeName(name) { + // Encode slashes as `encodeURIComponent('/')` + return name.replace(/\//g, '%2F'); +} + +let namePrefix = ''; + +/** + * A helper to define a category of visual tests. + * + * @param name The name of the category of test. + * @param callback A callback that calls `visualTest` a number of times to define + * visual tests within this suite. + * @param [options] An options object with optional additional settings. Set its + * key `focus` to true to only run this test, or its `skip` key to skip it. + */ +window.visualSuite = function( + name, + callback, + { focus = false, skip = false } = {} +) { + const lastPrefix = namePrefix; + namePrefix += escapeName(name) + '/'; + + let suiteFn = suite; + if (focus) { + suiteFn = suiteFn.only; + } + if (skip) { + suiteFn = suiteFn.skip; + } + suiteFn(name, callback); + + namePrefix = lastPrefix; +}; + +window.checkMatch = function(actual, expected, p5) { + const maxSide = 50; + const scale = Math.min(maxSide/expected.width, maxSide/expected.height); + for (const img of [actual, expected]) { + img.resize( + Math.ceil(img.width * scale), + Math.ceil(img.height * scale) + ); + } + const diff = p5.createImage(actual.width, actual.height); + diff.drawingContext.drawImage(actual.canvas, 0, 0); + diff.drawingContext.globalCompositeOperation = 'difference'; + diff.drawingContext.drawImage(expected.canvas, 0, 0); + diff.filter(p5.ERODE, false); + diff.loadPixels(); + + let ok = true; + for (let i = 0; i < diff.pixels.length; i++) { + if (i % 4 === 3) continue; // Skip alpha checks + if (Math.abs(diff.pixels[i]) > 10) { + ok = false; + break; + } + } + return { ok, diff }; +}; + +/** + * A helper to define a visual test, where we will assert that a sketch matches + * screenshots saved ahead of time of what the test should look like. + * + * When defining a new test, run the tests once to generate initial screenshots. + * + * To regenerate screenshots for a test, delete its screenshots folder in + * the test/unit/visual/screenshots directory, and rerun the tests. + * + * @param testName The display name of a test. This also links the test to its + * expected screenshot, so make sure to rename the screenshot folder after + * renaming a test. + * @param callback A callback to set up the test case. It takes two parameters: + * first is `p5`, a reference to the p5 instance, and second is `screenshot`, a + * function to grab a screenshot of the canvas. It returns either nothing, or a + * Promise that resolves when all screenshots have been taken. + * @param [options] An options object with optional additional settings. Set its + * key `focus` to true to only run this test, or its `skip` key to skip it. + */ +window.visualTest = function( + testName, + callback, + { focus = false, skip = false } = {} +) { + const name = namePrefix + escapeName(testName); + let suiteFn = suite; + if (focus) { + suiteFn = suiteFn.only; + } + if (skip) { + suiteFn = suiteFn.skip; + } + + suiteFn(testName, function() { + let myp5; + + setup(function() { + return new Promise(res => { + myp5 = new p5(function(p) { + p.setup = function() { + res(); + }; + }); + }); + }); + + teardown(function() { + myp5.remove(); + }); + + test('matches expected screenshots', async function() { + let expectedScreenshots; + try { + metadata = await fetch( + `unit/visual/screenshots/${name}/metadata.json` + ).then(res => res.json()); + expectedScreenshots = metadata.numScreenshots; + } catch (e) { + expectedScreenshots = 0; + } + + if (!window.shouldGenerateScreenshots && !expectedScreenshots) { + // If running on CI, all expected screenshots should already + // be generated + throw new Error('No expected screenshots found'); + } + + const actual = []; + + // Generate screenshots + await callback(myp5, () => { + actual.push(myp5.get()); + }); + + if (expectedScreenshots && actual.length !== expectedScreenshots) { + throw new Error( + `Expected ${expectedScreenshots} screenshot(s) but generated ${actual.length}` + ); + } + if (!expectedScreenshots) { + writeFile( + `unit/visual/screenshots/${name}/metadata.json`, + JSON.stringify({ numScreenshots: actual.length }, null, 2) + ); + } + + const expectedFilenames = actual.map( + (_, i) => `unit/visual/screenshots/${name}/${i.toString().padStart(3, '0')}.png` + ); + const expected = expectedScreenshots + ? ( + await Promise.all( + expectedFilenames.map(path => new Promise((resolve, reject) => { + myp5.loadImage(path, resolve, reject); + })) + ) + ) + : []; + + for (let i = 0; i < actual.length; i++) { + if (expected[i]) { + if (!checkMatch(actual[i], expected[i], myp5).ok) { + throw new ScreenshotError( + `Screenshots do not match! Expected:\n${toBase64(expected[i])}\n\nReceived:\n${toBase64(actual[i])}\n\n` + + 'If this is unexpected, paste these URLs into your browser to inspect them, or run grunt yui:dev and go to http://127.0.0.1:9001/test/visual.html.\n\n' + + `If this change is expected, please delete the test/unit/visual/screenshots/${name} folder and run tests again to generate a new screenshot.`, + actual[i], + expected[i] + ); + } + } else { + writeImageFile(expectedFilenames[i], toBase64(actual[i])); + } + } + }); + }); +}; diff --git a/test/visual.html b/test/visual.html new file mode 100644 index 0000000000..0e25bdf88a --- /dev/null +++ b/test/visual.html @@ -0,0 +1,16 @@ + + + + + p5.js Visual Test Runner + + + +

p5.js Visual Test Runner

+

+ + + + + + diff --git a/test/visual/style.css b/test/visual/style.css new file mode 100644 index 0000000000..3b087f3db1 --- /dev/null +++ b/test/visual/style.css @@ -0,0 +1,45 @@ +body { + font-family: sans-serif; +} + +h4 { + font-weight: normal; + margin-bottom: 10px; + margin-top: 0; +} + +#metrics { + color: #777; + text-decoration: italic; +} + +.suite { + padding-left: 10px; + border-left: 2px solid rgba(0,0,0,0.2); + margin-bottom: 30px; +} +.skipped { + opacity: 0.5; +} +.suite.focused { + border-left-color: #2B2; +} +.suite.failed { + border-left-color: #F00; +} + +.failed { + color: #F00; +} + +.screenshot img { + border: 2px solid #000; + margin-right: 5px; +} +.screenshot.failed img { + border-color: #F00; +} + +.diff { + background: #000; +} diff --git a/test/visual/visualTestList.js b/test/visual/visualTestList.js new file mode 100644 index 0000000000..7a008a20ef --- /dev/null +++ b/test/visual/visualTestList.js @@ -0,0 +1,8 @@ +// List all visual test files here that should be manually run +const visualTestList = ['webgl']; + +for (const file of visualTestList) { + document.write( + `` + ); +} diff --git a/test/visual/visualTestRunner.js b/test/visual/visualTestRunner.js new file mode 100644 index 0000000000..ec7106b7ff --- /dev/null +++ b/test/visual/visualTestRunner.js @@ -0,0 +1,125 @@ +let parentEl = document.body; +let skipping = false; +let setups = []; +let teardowns = []; +const tests = []; + +window.devicePixelRatio = 1; + +// Force default antialiasing to match Chrome in puppeteer +const origSetAttributeDefaults = p5.RendererGL.prototype._setAttributeDefaults; +p5.RendererGL.prototype._setAttributeDefaults = function(pInst) { + origSetAttributeDefaults(pInst); + pInst._glAttributes = Object.assign({}, pInst._glAttributes); + pInst._glAttributes.antialias = true; +}; + +window.suite = function(name, callback) { + const prevSetups = setups; + const prevTeardowns = teardowns; + const prevParent = parentEl; + const suiteEl = document.createElement('div'); + suiteEl.classList.add('suite'); + const title = document.createElement('h4'); + title.innerText = decodeURIComponent(name); + suiteEl.appendChild(title); + parentEl.appendChild(suiteEl); + + parentEl = suiteEl; + setups = [...setups]; + teardowns = [...teardowns]; + callback(); + + parentEl = prevParent; + setups = prevSetups; + teardowns = prevTeardowns; + return suiteEl; +}; +window.suite.skip = function(name, callback) { + const prevSkipping = skipping; + skipping = true; + const el = window.suite(name, callback); + el.classList.add('skipped'); + skipping = prevSkipping; +}; +window.suite.only = function(name, callback) { + const el = window.suite(name, callback); + el.classList.add('focused'); +}; + +window.setup = function(cb) { + if (!cb) return; + setups.push(cb); +}; + +window.teardown = function(cb) { + if (!cb) return; + teardowns.push(cb); +}; + +window.test = function(_name, callback) { + const testEl = document.createElement('div'); + testEl.classList.add('test'); + parentEl.appendChild(testEl); + const currentParent = parentEl; + const testSetups = setups; + const testTeardowns = teardowns; + if (!skipping) { + tests.push(async function() { + const prevCheckMatch = window.checkMatch; + window.checkMatch = function(actual, expected, p5) { + let { ok, diff } = prevCheckMatch(actual, expected, p5); + + const screenshot = document.createElement('div'); + screenshot.classList.add('screenshot'); + const actualPreview = document.createElement('img'); + actualPreview.setAttribute('src', actual.canvas.toDataURL()); + actualPreview.setAttribute('title', 'Received'); + const expectedPreview = document.createElement('img'); + expectedPreview.setAttribute('src', expected.canvas.toDataURL()); + expectedPreview.setAttribute('title', 'Expected'); + const diffPreview = document.createElement('img'); + diffPreview.setAttribute('src', diff.canvas.toDataURL()); + diffPreview.setAttribute('title', 'Difference'); + diffPreview.classList.add('diff'); + screenshot.appendChild(actualPreview); + screenshot.appendChild(expectedPreview); + screenshot.appendChild(diffPreview); + if (!ok) { + screenshot.classList.add('failed'); + currentParent.classList.add('failed'); + } + testEl.appendChild(screenshot); + return { ok, diff }; + }; + try { + for (const setup of testSetups) { + await setup(); + } + await callback(); + } catch (e) { + if (!(e instanceof ScreenshotError)) { + const p = document.createElement('p'); + p.innerText = e.toString(); + testEl.appendChild(p); + } + testEl.classList.add('failed'); + } + for (const teardown of testTeardowns) { + await teardown(); + } + window.checkMatch = prevCheckMatch; + }); + } +}; + +window.addEventListener('load', async function() { + for (const test of tests) { + await test(); + } + + const numTotal = document.querySelectorAll('.test').length; + const numFailed = document.querySelectorAll('.test.failed').length; + document.getElementById('metrics').innerHTML = + `${numTotal - numFailed} passed out of ${numTotal}`; +}); From 5f89e239f12a608417ff132d971ccddcdc792096 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 2 Dec 2023 13:56:56 -0500 Subject: [PATCH 2/2] Rename function to avoid name conflict --- tasks/test/mocha-chrome.js | 2 +- test/unit/visual/visualTest.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/test/mocha-chrome.js b/tasks/test/mocha-chrome.js index d2d998fe86..dc247ab87c 100644 --- a/tasks/test/mocha-chrome.js +++ b/tasks/test/mocha-chrome.js @@ -43,7 +43,7 @@ module.exports = function(grunt) { 'base64' ); }); - await page.exposeFunction('writeFile', function(filename, data) { + await page.exposeFunction('writeTextFile', function(filename, data) { fs.mkdirSync('test/' + path.dirname(filename), { recursive: true }); fs.writeFileSync( 'test/' + filename, diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index 8c37829f14..24dbec2f53 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -157,7 +157,7 @@ window.visualTest = function( ); } if (!expectedScreenshots) { - writeFile( + writeTextFile( `unit/visual/screenshots/${name}/metadata.json`, JSON.stringify({ numScreenshots: actual.length }, null, 2) );