diff --git a/.babelrc b/.babelrc
index 97242d46ff06d0c89e2652bd3bd629e722def056..0cb0bd16047bb69cd9786166ba7709906320358e 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,7 @@
 {
     "plugins": [
         "syntax-dynamic-import",
+        "transform-async-to-generator",
         "transform-object-rest-spread",
         ["react-intl", {
             "messagesDir": "./translations/messages/"
diff --git a/.travis.yml b/.travis.yml
index cc8a5822a40f0588d27d8e81a5c613b940fcde2d..ec6ccee6c4196c66b195391b41637c38633ca256 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,11 @@
 language: node_js
 sudo: false
 dist: trusty
+addons:
+    chrome: stable
+before_script:
+  - "export DISPLAY=:99.0"
+  - "sh -e /etc/init.d/xvfb start"
 node_js:
 - 6
 cache:
diff --git a/README.md b/README.md
index b478b1e47504962bd77f2cc115da6e57c2dabea1..c036a170930285319aff1fe3f5b348883df9ec65 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Then go to [http://localhost:8601/](http://localhost:8601/) - the playground out
 ## Testing
 NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe`  instead of Git Bash/MINGW64.
 
-Run linter, unit tests, and build.
+Run linter, unit tests, build, and integration tests.
 ```bash
 npm test
 ```
@@ -46,6 +46,11 @@ Run unit tests in watch mode (watches for code changes and continuously runs tes
 npm run unit-test -- --watch
 ```
 
+Run integration tests in isolation.
+```bash
+npm run integration-test
+```
+
 You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests.
 
 ## Publishing to GitHub Pages
diff --git a/package.json b/package.json
index e51c1bcbc18109ae0d65066220e0d80d7e75d8c7..bad718d7eecc7b33968243a26795c72d09fe0029 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,9 @@
     "i18n:src": "babel src > tmp.js && rimraf tmp.js && ./scripts/build-i18n-source.js ./translations/messages/ ./translations/",
     "lint": "eslint . --ext .js,.jsx",
     "start": "npm run i18n:msgs && webpack-dev-server",
-    "unit-test": "jest",
-    "test": "npm run lint && npm run unit-test && npm run build",
+    "unit-test": "jest test/unit",
+    "integration-test": "npm run build && jest test/integration",
+    "test": "npm run lint && npm run unit-test && npm run integration-test",
     "watch": "webpack --progress --colors --watch"
   },
   "author": "Massachusetts Institute of Technology",
@@ -34,9 +35,11 @@
     "babel-loader": "^7.0.0",
     "babel-plugin-react-intl": "2.3.1",
     "babel-plugin-syntax-dynamic-import": "6.18.0",
+    "babel-plugin-transform-async-to-generator": "^6.24.1",
     "babel-plugin-transform-object-rest-spread": "^6.22.0",
     "babel-preset-es2015": "^6.22.0",
     "babel-preset-react": "^6.22.0",
+    "chromedriver": "^2.31.0",
     "classnames": "2.2.5",
     "copy-webpack-plugin": "4.0.1",
     "css-loader": "0.28.3",
@@ -83,6 +86,7 @@
     "scratch-render": "latest",
     "scratch-storage": "^0.2.0",
     "scratch-vm": "latest",
+    "selenium-webdriver": "^3.5.0",
     "style-loader": "^0.18.0",
     "svg-to-image": "1.1.3",
     "svg-url-loader": "2.1.0",
diff --git a/src/lib/libraries/costumes.json b/src/lib/libraries/costumes.json
index 370f2d8b95b60b31f319982dd5bf547f6e1b4595..f359a3f16670dd17f324c32b6e4585d2d6395678 100644
--- a/src/lib/libraries/costumes.json
+++ b/src/lib/libraries/costumes.json
@@ -7717,7 +7717,7 @@
             109,
             32
         ],
-        "md5": "4cddf35440090d30d4de188625d609c9.svg",
+        "md5": "0a54c19714962c197532f68c56fde123.svg",
         "type": "costume",
         "name": "text awesome",
         "tags": [
@@ -7731,7 +7731,7 @@
             135,
             24
         ],
-        "md5": "6f54c5de1985d7f23a24341e1688c4b0.svg",
+        "md5": "97c9e6ef78d4f1987fd8c6e5042963cb.svg",
         "type": "costume",
         "name": "text keep scratching",
         "tags": [
@@ -7745,7 +7745,7 @@
             172,
             30
         ],
-        "md5": "e634628e4b948573710f7e4a4f50659b.svg",
+        "md5": "9fde2e190a9d01c08737dff291bdd4a8.svg",
         "type": "costume",
         "name": "text valentine's day",
         "tags": [
@@ -7759,7 +7759,7 @@
             165,
             34
         ],
-        "md5": "7ebe62c6f2b7c8b644d2abe036b8f69e.svg",
+        "md5": "0176a402a133f7b833da61b890e0d73e.svg",
         "type": "costume",
         "name": "text Halloween",
         "tags": [
@@ -10629,4 +10629,4 @@
             "vector"
         ]
     }
-]
\ No newline at end of file
+]
diff --git a/test/helpers/selenium-helpers.js b/test/helpers/selenium-helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..5ee0af01e65a379c5dee5f92f26536e3aa98dab4
--- /dev/null
+++ b/test/helpers/selenium-helpers.js
@@ -0,0 +1,58 @@
+/* eslint-env jest */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // eslint-disable-line no-undef
+
+import webdriver from 'selenium-webdriver';
+
+const {By, until} = webdriver;
+
+const driver = new webdriver.Builder()
+    .forBrowser('chrome')
+    .build();
+
+const findByXpath = (xpath) => {
+    return driver.wait(until.elementLocated(By.xpath(xpath), 5 * 1000));
+};
+
+const clickXpath = (xpath) => {
+    return findByXpath(xpath).then(el => el.click());
+};
+
+const clickText = (text) => {
+    return clickXpath(`//*[contains(text(), '${text}')]`);
+};
+
+const clickButton = (text) => {
+    return clickXpath(`//button[contains(text(), '${text}')]`);
+};
+
+const getLogs = (whitelist) => {
+    return driver.manage()
+        .logs()
+        .get('browser')
+        .then((entries) => {
+            return entries.filter((entry) => {
+                const message = entry.message;
+                for (let i = 0; i < whitelist.length; i++) {
+                    if (message.indexOf(whitelist[i]) !== -1) {
+                        // eslint-disable-next-line no-console
+                        console.warn('Ignoring whitelisted error: ' + whitelist[i]);
+                        return false;
+                    } else if (entry.level !== 'SEVERE') {
+                        // eslint-disable-next-line no-console
+                        console.warn('Ignoring non-SEVERE entry: ' + message);
+                        return false;
+                    }
+                    return true;
+                }
+            });
+        });
+};
+
+export {
+    clickText,
+    clickButton,
+    clickXpath,
+    driver,
+    findByXpath,
+    getLogs
+};
diff --git a/test/integration/test.js b/test/integration/test.js
new file mode 100644
index 0000000000000000000000000000000000000000..7e3e60a651a6e7d64f06ad56c561058449432f20
--- /dev/null
+++ b/test/integration/test.js
@@ -0,0 +1,89 @@
+/* eslint-env jest */
+/* globals Promise */
+
+import path from 'path';
+import {
+    clickText,
+    clickButton,
+    clickXpath,
+    driver,
+    findByXpath,
+    getLogs
+} from '../helpers/selenium-helpers';
+
+const uri = path.resolve(__dirname, '../../build/index.html');
+
+const errorWhitelist = [
+    'The play() request was interrupted by a call to pause()'
+];
+
+describe('costumes, sounds and variables', () => {
+    afterAll(async () => {
+        await driver.quit();
+    });
+
+    test('Adding a costume', async () => {
+        await driver.get('file://' + uri);
+        await clickText('Costumes');
+        await clickText('Add Costume');
+        const el = await findByXpath("//input[@placeholder='what are you looking for?']");
+        await el.sendKeys('abb');
+        await clickText('abby-a'); // Should close the modal, then click the costumes in the selector
+        await clickText('costume1');
+        await clickText('abby-a');
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
+    test('Adding a sound', async () => {
+        await driver.get('file://' + uri);
+        await clickText('Sounds');
+        await clickText('Add Sound');
+        const el = await findByXpath("//input[@placeholder='what are you looking for?']");
+        await el.sendKeys('chom');
+        await clickText('chomp'); // Should close the modal, then click the sounds in the selector
+        await clickText('meow');
+        await clickText('chomp');
+        await clickXpath('//button[@title="Play"]');
+        await clickText('meow');
+        await clickXpath('//button[@title="Play"]');
+
+        await clickText('Louder');
+        await clickText('Softer');
+        await clickText('Faster');
+        await clickText('Slower');
+        await clickText('Robot');
+        await clickText('Echo');
+        await clickText('Reverse');
+
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
+    test('Load a project by ID', async () => {
+        const projectId = '96708228';
+        await driver.get('file://' + uri + '#' + projectId);
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        await clickXpath('//img[@title="Go"]');
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        await clickXpath('//img[@title="Stop"]');
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
+    test('Creating variables', async () => {
+        await driver.get('file://' + uri);
+        await clickText('Blocks');
+        await clickText('Data');
+        await clickText('Create variable...');
+        let el = await findByXpath("//input[@placeholder='']");
+        await el.sendKeys('score');
+        await clickButton('OK');
+        await clickText('Create variable...');
+        el = await findByXpath("//input[@placeholder='']");
+        await el.sendKeys('second variable');
+        await clickButton('OK');
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+});