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