diff --git a/.travis.yml b/.travis.yml index 969909f79..1a04bcbe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,10 @@ language: node_js dist: trusty -addons: - chrome: stable node_js: -- 8 +- 10 - node env: - NODE_ENV=production -before_install: - - google-chrome-stable --headless --no-sandbox --remote-debugging-port=9222 & install: - npm --production=false install - npm --production=false update @@ -24,7 +20,7 @@ jobs: - npm run docs - npm run tap - stage: deploy - node_js: 8 + node_js: 10 script: npm run build before_deploy: - VPKG=$($(npm bin)/json -f package.json version) diff --git a/package.json b/package.json index 91c1c6de3..b69fef97b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "babel-loader": "^7.1.4", "babel-polyfill": "^6.22.0", "babel-preset-env": "^1.6.1", - "chromeless": "^1.5.1", "copy-webpack-plugin": "^4.5.1", "docdash": "^0.4.0", "eslint": "^4.6.1", @@ -37,6 +36,7 @@ "gh-pages": "^1.0.0", "jsdoc": "^3.5.5", "json": "^9.0.4", + "playwright-chromium": "^1.0.1", "scratch-vm": "0.2.0-prerelease.20191227164934", "tap": "^11.0.0", "travis-after-all": "^1.4.4", diff --git a/test/helper/page-util.js b/test/helper/page-util.js new file mode 100644 index 000000000..25f5b007e --- /dev/null +++ b/test/helper/page-util.js @@ -0,0 +1,54 @@ +/* global window, VirtualMachine, ScratchStorage, ScratchSVGRenderer */ +/* eslint-env browser */ + +// Wait for all SVG skins to be loaded. +// TODO: this is extremely janky and should be removed once vm.loadProject waits for SVG skins to load +// https://github.com/LLK/scratch-render/issues/563 +window.waitForSVGSkinLoad = renderer => new Promise(resolve => { + // eslint-disable-next-line prefer-const + let interval; + + const waitInner = () => { + let numSVGSkins = 0; + let numLoadedSVGSkins = 0; + for (const skin of renderer._allSkins) { + if (skin.constructor.name !== 'SVGSkin') continue; + numSVGSkins++; + if (skin._svgRenderer.loaded) numLoadedSVGSkins++; + } + + if (numSVGSkins === numLoadedSVGSkins) { + clearInterval(interval); + resolve(); + } + }; + + interval = setInterval(waitInner, 1); +}); + +window.loadFileInputIntoVM = (fileInput, vm, render) => { + const reader = new FileReader(); + return new Promise(resolve => { + reader.onload = () => { + vm.start(); + vm.loadProject(reader.result) + .then(() => window.waitForSVGSkinLoad(render)) + .then(() => { + resolve(); + }); + }; + reader.readAsArrayBuffer(fileInput.files[0]); + }); +}; + +window.initVM = render => { + const vm = new VirtualMachine(); + const storage = new ScratchStorage(); + + vm.attachStorage(storage); + vm.attachRenderer(render); + vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer()); + vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter()); + + return vm; +}; diff --git a/test/integration/cpu-render.html b/test/integration/cpu-render.html index d6d363084..26a79c3f4 100644 --- a/test/integration/cpu-render.html +++ b/test/integration/cpu-render.html @@ -2,6 +2,7 @@ <script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script> <script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script> <script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script> + <script src="../helper/page-util.js"></script> <!-- note: this uses the BUILT version of scratch-render! make sure to npm run build --> <script src="../../dist/web/scratch-render.js"></script> @@ -17,38 +18,18 @@ window.devicePixelRatio = 1; const gpuCanvas = document.getElementById('test'); var render = new ScratchRender(gpuCanvas); - var vm = new VirtualMachine(); - var storage = new ScratchStorage(); + var vm = initVM(render); - vm.attachStorage(storage); - vm.attachRenderer(render); - vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer()); - vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter()); - - document.getElementById('file').addEventListener('click', e => { - document.body.removeChild(document.getElementById('loaded')); - }); - - document.getElementById('file').addEventListener('change', e => { - const reader = new FileReader(); - const thisFileInput = e.target; - reader.onload = () => { - vm.start(); - vm.loadProject(reader.result) - .then(() => { - // we add a `#loaded` div to our document, the integration suite - // waits for that element to show up to assume the vm is ready - // to play! - const div = document.createElement('div'); - div.id='loaded'; - document.body.appendChild(div); - vm.greenFlag(); - setTimeout(() => { - renderCpu(); - }, 1000); - }); - }; - reader.readAsArrayBuffer(thisFileInput.files[0]); + const fileInput = document.getElementById('file'); + const loadFile = loadFileInputIntoVM.bind(null, fileInput, vm, render); + fileInput.addEventListener('change', e => { + loadFile() + .then(() => { + vm.greenFlag(); + setTimeout(() => { + renderCpu(); + }, 1000); + }); }); const cpuCanvas = document.getElementById('cpu'); diff --git a/test/integration/index.html b/test/integration/index.html index e3d8dd838..114fa5b9d 100644 --- a/test/integration/index.html +++ b/test/integration/index.html @@ -2,6 +2,7 @@ <script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script> <script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script> <script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script> + <script src="../helper/page-util.js"></script> <!-- note: this uses the BUILT version of scratch-render! make sure to npm run build --> <script src="../../dist/web/scratch-render.js"></script> @@ -15,39 +16,13 @@ var canvas = document.getElementById('test'); var render = new ScratchRender(canvas); - var vm = new VirtualMachine(); - var storage = new ScratchStorage(); + var vm = initVM(render); var mockMouse = data => vm.runtime.postIOData('mouse', { canvasWidth: canvas.width, canvasHeight: canvas.height, ...data, }); - vm.attachStorage(storage); - vm.attachRenderer(render); - vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer()); - vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter()); - - document.getElementById('file').addEventListener('click', e => { - document.body.removeChild(document.getElementById('loaded')); - }); - - document.getElementById('file').addEventListener('change', e => { - const reader = new FileReader(); - const thisFileInput = e.target; - reader.onload = () => { - vm.start(); - vm.loadProject(reader.result) - .then(() => { - // we add a `#loaded` div to our document, the integration suite - // waits for that element to show up to assume the vm is ready - // to play! - const div = document.createElement('div'); - div.id='loaded'; - document.body.appendChild(div); - }); - }; - reader.readAsArrayBuffer(thisFileInput.files[0]); - }); + const loadFile = loadFileInputIntoVM.bind(null, document.getElementById('file'), vm, render); </script> </body> diff --git a/test/integration/pick-tests.js b/test/integration/pick-tests.js index 6d3222e50..4012bcdad 100644 --- a/test/integration/pick-tests.js +++ b/test/integration/pick-tests.js @@ -1,29 +1,34 @@ /* global vm, render, Promise */ -const {Chromeless} = require('chromeless'); +const {chromium} = require('playwright-chromium'); const test = require('tap').test; const path = require('path'); -const chromeless = new Chromeless(); const indexHTML = path.resolve(__dirname, 'index.html'); const testDir = (...args) => path.resolve(__dirname, 'pick-tests', ...args); -const runFile = (file, action, script) => +const runFile = async (file, action, page, script) => { // start each test by going to the index.html, and loading the scratch file - chromeless.goto(`file://${indexHTML}`) - .setFileInput('#file', testDir(file)) - // the index.html handler for file input will add a #loaded element when it - // finishes. - .wait('#loaded') - .evaluate(`function () {return (${script})(${action});}`) -; + await page.goto(`file://${indexHTML}`); + const fileInput = await page.$('#file'); + await fileInput.setInputFiles(testDir(file)); + + await page.evaluate(() => + // `loadFile` is defined on the page itself. + // eslint-disable-next-line no-undef + loadFile() + ); + return page.evaluate(`(function () {return (${script})(${action});})()`); +}; // immediately invoked async function to let us wait for each test to finish before starting the next. (async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); const testOperation = async function (name, action, expect) { await test(name, async t => { - const results = await runFile('test-mouse-touch.sb2', action, boundAction => { + const results = await runFile('test-mouse-touch.sb2', action, page, boundAction => { vm.greenFlag(); const sendResults = []; @@ -97,5 +102,5 @@ const runFile = (file, action, script) => } // close the browser window we used - await chromeless.end(); + await browser.close(); })(); diff --git a/test/integration/scratch-tests.js b/test/integration/scratch-tests.js index f11bc5ba7..bad0774d2 100644 --- a/test/integration/scratch-tests.js +++ b/test/integration/scratch-tests.js @@ -1,54 +1,56 @@ /* global vm, Promise */ -const {Chromeless} = require('chromeless'); +const {chromium} = require('playwright-chromium'); const test = require('tap').test; const path = require('path'); const fs = require('fs'); -const chromeless = new Chromeless(); const indexHTML = path.resolve(__dirname, 'index.html'); const testDir = (...args) => path.resolve(__dirname, 'scratch-tests', ...args); -const testFile = file => test(file, async t => { +const testFile = (file, page) => test(file, async t => { // start each test by going to the index.html, and loading the scratch file - const says = await chromeless.goto(`file://${indexHTML}`) - .setFileInput('#file', testDir(file)) - // the index.html handler for file input will add a #loaded element when it - // finishes. - .wait('#loaded') - .evaluate(() => { - // This function is run INSIDE the integration chrome browser via some - // injection and .toString() magic. We can return some "simple data" - // back across as a promise, so we will just log all the says that happen - // for parsing after. - - // this becomes the `says` in the outer scope - const messages = []; - const TIMEOUT = 5000; - - vm.runtime.on('SAY', (_, __, message) => { - messages.push(message); - }); + await page.goto(`file://${indexHTML}`); + const fileInput = await page.$('#file'); + await fileInput.setInputFiles(testDir(file)); + await page.evaluate(() => + // `loadFile` is defined on the page itself. + // eslint-disable-next-line no-undef + loadFile() + ); + const says = await page.evaluate(() => { + // This function is run INSIDE the integration chrome browser via some + // injection and .toString() magic. We can return some "simple data" + // back across as a promise, so we will just log all the says that happen + // for parsing after. + + // this becomes the `says` in the outer scope + const messages = []; + const TIMEOUT = 5000; + + vm.runtime.on('SAY', (_, __, message) => { + messages.push(message); + }); - vm.greenFlag(); - const startTime = Date.now(); - - return Promise.resolve() - .then(async () => { - // waiting for all threads to complete, then we return - while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) { - if ((Date.now() - startTime) >= TIMEOUT) { - // if we push the message after end, the failure from tap is not very useful: - // "not ok test after end() was called" - messages.unshift(`fail Threads still running after ${TIMEOUT}ms`); - break; - } - - await new Promise(resolve => setTimeout(resolve, 50)); + vm.greenFlag(); + const startTime = Date.now(); + + return Promise.resolve() + .then(async () => { + // waiting for all threads to complete, then we return + while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) { + if ((Date.now() - startTime) >= TIMEOUT) { + // if we push the message after end, the failure from tap is not very useful: + // "not ok test after end() was called" + messages.unshift(`fail Threads still running after ${TIMEOUT}ms`); + break; } - return messages; - }); - }); + await new Promise(resolve => setTimeout(resolve, 50)); + } + + return messages; + }); + }); // Map string messages to tap reporting methods. This will be used // with events from scratch's runtime emitted on block instructions. @@ -103,13 +105,16 @@ const testFile = file => test(file, async t => { // immediately invoked async function to let us wait for each test to finish before starting the next. (async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + const files = fs.readdirSync(testDir()) .filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3')); for (const file of files) { - await testFile(file); + await testFile(file, page); } // close the browser window we used - await chromeless.end(); + await browser.close(); })();