diff --git a/.eslintrc.js b/.eslintrc.js index c16b6bca..ad845bc6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,4 @@ module.exports = { root: true, - extends: ['scratch', 'scratch/node'] + extends: ['scratch', 'scratch/node', 'scratch/es6'] }; diff --git a/.travis.yml b/.travis.yml index 640c9dd5..47740dc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,14 @@ language: node_js +dist: trusty +addons: + chrome: stable node_js: - 8 - 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 diff --git a/package.json b/package.json index 416615c2..9f410c2c 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "prepublish": "npm run build", "prepublish-watch": "npm run watch", "start": "webpack-dev-server", - "tap": "./node_modules/.bin/tap ./test/unit/*.js", - "test": "npm run lint && npm run docs && npm run tap", + "tap": "tap test/unit test/integration", + "test": "npm run lint && npm run docs && npm run build && npm run tap", "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"", "watch": "webpack --progress --colors --watch --watch-poll" }, @@ -30,6 +30,7 @@ "babel-polyfill": "^6.22.0", "babel-preset-es2015": "^6.22.0", "base64-loader": "^1.0.0", + "chromeless": "^1.5.1", "copy-webpack-plugin": "^4.0.1", "docdash": "^0.4.0", "eslint": "^4.6.1", @@ -42,7 +43,9 @@ "json": "^9.0.4", "linebreak": "0.3.0", "raw-loader": "^0.5.1", + "scratch-storage": "^0.4.0", "scratch-svg-renderer": "0.1.0-prerelease.20180423193917", + "scratch-vm": "0.1.0-prerelease.1524520946", "tap": "^11.0.0", "travis-after-all": "^1.4.4", "twgl.js": "4.4.0", diff --git a/test/integration/index.html b/test/integration/index.html new file mode 100644 index 00000000..a1f2af40 --- /dev/null +++ b/test/integration/index.html @@ -0,0 +1,43 @@ +<body> + <script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script> + <script src="../../node_modules/scratch-storage/dist/web/scratch-storage.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> + + <canvas id="test" width="480" height="360"></canvas> + <input type="file" id="file" name="file"> + + <script> + // These variables are going to be available in the "window global" intentionally. + // Allows you easy access to debug with `vm.greenFlag()` etc. + + var render = new ScratchRender(document.getElementById('test')); + var vm = new VirtualMachine(); + var storage = new ScratchStorage(); + + vm.attachStorage(storage); + vm.attachRenderer(render); + + 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]); + }); + </script> +</body> diff --git a/test/integration/scratch-tests.js b/test/integration/scratch-tests.js new file mode 100644 index 00000000..fa9321db --- /dev/null +++ b/test/integration/scratch-tests.js @@ -0,0 +1,113 @@ +/* global vm, Promise */ +const {Chromeless} = require('chromeless'); +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 => { + // 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); + }); + + vm.greenFlag(); + const startTime = Date.now(); + + return Promise.resolve() + .then(async () => { + // waiting for all threads to complete, then we return + while (vm.runtime.threads.length > 0) { + if ((Date.now() - startTime) >= TIMEOUT) { + messages.push(`fail Threads still running after ${TIMEOUT}ms`); + break; + } + + 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. + let didPlan = false; + let didEnd = false; + const reporters = { + comment (message) { + t.comment(message); + }, + pass (reason) { + t.pass(reason); + }, + fail (reason) { + t.fail(reason); + }, + plan (count) { + didPlan = true; + t.plan(Number(count)); + }, + end () { + didEnd = true; + t.end(); + } + }; + + // loop over each "SAY" we caught from the VM and use the reporters + says.forEach(text => { + // first word of the say is going to be a "command" + const command = text.split(/\s+/, 1)[0].toLowerCase(); + if (reporters[command]) { + return reporters[command](text.substring(command.length).trim()); + } + + // Default to a comment with the full text if we didn't match + // any command prefix + return reporters.comment(text); + }); + + if (!didPlan) { + t.comment('did not say "plan NUMBER_OF_TESTS"'); + } + + // End must be called so that tap knows the test is done. If + // the test has a SAY "end" block but that block did not + // execute, this explicit failure will raise that issue so + // it can be resolved. + if (!didEnd) { + t.fail('did not say "end"'); + t.end(); + } +}); + +// immediately invoked async function to let us wait for each test to finish before starting the next. +(async () => { + const files = fs.readdirSync(testDir()) + .filter(uri => uri.endsWith('.sb2') || uri.endsWidth('.sb3')); + + for (const file of files) { + await testFile(file); + } + + // close the browser window we used + await chromeless.end(); +})(); diff --git a/test/integration/scratch-tests/cat-touches-box.sb2 b/test/integration/scratch-tests/cat-touches-box.sb2 new file mode 100644 index 00000000..c21b67ea Binary files /dev/null and b/test/integration/scratch-tests/cat-touches-box.sb2 differ diff --git a/test/integration/scratch-tests/ghost-hidden-collide.sb2 b/test/integration/scratch-tests/ghost-hidden-collide.sb2 new file mode 100644 index 00000000..bd052093 Binary files /dev/null and b/test/integration/scratch-tests/ghost-hidden-collide.sb2 differ diff --git a/test/integration/scratch-tests/tippy-toe-collision.sb2 b/test/integration/scratch-tests/tippy-toe-collision.sb2 new file mode 100644 index 00000000..0646ea0f Binary files /dev/null and b/test/integration/scratch-tests/tippy-toe-collision.sb2 differ