diff --git a/package.json b/package.json
index da6248942f574c92d01025dc8744bf740403a7fc..72180c2b3e09aeedf805a76eea7b418f786eeca7 100644
--- a/package.json
+++ b/package.json
@@ -100,13 +100,13 @@
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "0.1.0-prerelease.20181023202904",
-    "scratch-blocks": "0.1.0-prerelease.1541019511",
+    "scratch-blocks": "0.1.0-prerelease.1541605931",
     "scratch-l10n": "3.0.20181031234255",
-    "scratch-paint": "0.2.0-prerelease.20181029151725",
-    "scratch-render": "0.1.0-prerelease.20181029125804",
+    "scratch-paint": "0.2.0-prerelease.20181105210331",
+    "scratch-render": "0.1.0-prerelease.20181102130522",
     "scratch-storage": "1.1.0",
     "scratch-svg-renderer": "0.2.0-prerelease.20181101210634",
-    "scratch-vm": "0.2.0-prerelease.20181101191128",
+    "scratch-vm": "0.2.0-prerelease.20181107153250",
     "selenium-webdriver": "3.6.0",
     "startaudiocontext": "1.2.1",
     "style-loader": "^0.23.0",
diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css
index c4f00a741912c0ea92b73d5f387b759f707dce4b..0787a9fb12436804149417d930db8afd01de5c71 100644
--- a/src/components/alerts/alert.css
+++ b/src/components/alerts/alert.css
@@ -30,7 +30,29 @@
 }
 
 .alert-close-button {
+    outline-style:none;
+}
+
+.alert-close-button-container {
     margin-top: 7px;
     margin-right: 4px;
     outline-style:none;
+    width: 30px;
+    height: 30px;
+}
+
+.connection-button {
+    padding: 0.55rem 0.9rem;
+    border-radius: 0.35rem;
+    background: #FF8C1A;
+    color: white;
+    font-weight: 700;
+    font-size: 0.77rem;
+    margin: 0.25rem;
+    border: none;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    margin-right: 13px;
+    outline-style:none;
 }
diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx
index d0f65309e086f3cc2582131ce21677c2d9a4845e..2ec848984c77aaac28a9a85f6a3bce048089b617 100644
--- a/src/components/alerts/alert.jsx
+++ b/src/components/alerts/alert.jsx
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
 
 import Box from '../box/box.jsx';
 import CloseButton from '../close-button/close-button.jsx';
@@ -9,7 +10,9 @@ import styles from './alert.css';
 const AlertComponent = ({
     iconURL,
     message,
-    onCloseAlert
+    onCloseAlert,
+    onReconnect,
+    showReconnect
 }) => (
     <Box
         className={styles.alert}
@@ -23,19 +26,37 @@ const AlertComponent = ({
             ) : null}
             {message}
         </div>
-        <CloseButton
-            className={styles.alertCloseButton}
-            color={CloseButton.COLOR_ORANGE}
-            size={CloseButton.SIZE_LARGE}
-            onClick={onCloseAlert}
-        />
+        {showReconnect ? (
+            <button
+                className={styles.connectionButton}
+                onClick={onReconnect}
+            >
+                <FormattedMessage
+                    defaultMessage="Reconnect"
+                    description="Button to reconnect the device"
+                    id="gui.connection.reconnect"
+                />
+            </button>
+        ) : null}
+        <Box
+            className={styles.alertCloseButtonContainer}
+        >
+            <CloseButton
+                className={styles.alertCloseButton}
+                color={CloseButton.COLOR_ORANGE}
+                size={CloseButton.SIZE_LARGE}
+                onClick={onCloseAlert}
+            />
+        </Box>
     </Box>
 );
 
 AlertComponent.propTypes = {
     iconURL: PropTypes.string,
     message: PropTypes.string,
-    onCloseAlert: PropTypes.func.isRequired
+    onCloseAlert: PropTypes.func.isRequired,
+    onReconnect: PropTypes.func,
+    showReconnect: PropTypes.bool.isRequired
 };
 
 export default AlertComponent;
diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx
index 1835a50a820e7de90443449ecec60c54f33779f5..9a87f766c376a0562225e118c9ef53cea0d29f4f 100644
--- a/src/components/alerts/alerts.jsx
+++ b/src/components/alerts/alerts.jsx
@@ -15,10 +15,12 @@ const AlertsComponent = ({
     >
         {alertsList.map((a, index) => (
             <Alert
+                extensionId={a.extensionId}
                 iconURL={a.iconURL}
                 index={index}
                 key={index}
                 message={a.message}
+                showReconnect={a.showReconnect}
                 onCloseAlert={onCloseAlert}
             />
         ))}
diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css
index 33585ca90de66225f89228aa5aa9a35f6798adca..6eddfa905c4a6f79eb1017d149b7030e32aa278a 100644
--- a/src/components/backpack/backpack.css
+++ b/src/components/backpack/backpack.css
@@ -71,5 +71,21 @@
 .backpack-item {
     min-width: 4rem;
     margin: 0 0.25rem;
+}
+
+.backpack-item img {
     mix-blend-mode: multiply; /* Make white transparent for thumnbnails */
 }
+
+.more {
+    background: $motion-primary;
+    color: $ui-white;
+    border: none;
+    outline: none;
+    font-weight: bold;
+    border-radius: 0.5rem;
+    font-size: 0.85rem;
+    padding: 0.5rem;
+    margin: 0.5rem;
+    cursor: pointer;
+}
diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx
index f5f4500393bc73d80e888d7b7b90b97ad6cde567..f5d1a0ae280b81849055830aab20bd0b575a868a 100644
--- a/src/components/backpack/backpack.jsx
+++ b/src/components/backpack/backpack.jsx
@@ -25,10 +25,12 @@ const Backpack = ({
     error,
     expanded,
     loading,
+    showMore,
     onToggle,
     onDelete,
     onMouseEnter,
-    onMouseLeave
+    onMouseLeave,
+    onMore
 }) => (
     <div className={styles.backpackContainer}>
         <div
@@ -98,6 +100,18 @@ const Backpack = ({
                                         onDeleteButtonClick={onDelete}
                                     />
                                 ))}
+                                {showMore && (
+                                    <button
+                                        className={styles.more}
+                                        onClick={onMore}
+                                    >
+                                        <FormattedMessage
+                                            defaultMessage="More"
+                                            description="Load more from backpack"
+                                            id="gui.backpack.more"
+                                        />
+                                    </button>
+                                )}
                             </div>
                         ) : (
                             <div className={styles.statusMessage}>
@@ -129,9 +143,11 @@ Backpack.propTypes = {
     expanded: PropTypes.bool,
     loading: PropTypes.bool,
     onDelete: PropTypes.func,
+    onMore: PropTypes.func,
     onMouseEnter: PropTypes.func,
     onMouseLeave: PropTypes.func,
-    onToggle: PropTypes.func
+    onToggle: PropTypes.func,
+    showMore: PropTypes.bool
 };
 
 Backpack.defaultProps = {
@@ -140,6 +156,8 @@ Backpack.defaultProps = {
     dragOver: false,
     expanded: false,
     loading: false,
+    showMore: false,
+    onMore: null,
     onToggle: null
 };
 
diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index 3e4d763c4817107abd738ec6c4c8367aae8246d6..eb4f1c929a39974b58fe42c951057b2ab4454bc6 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -303,7 +303,7 @@ $fade-out-distance: 15px;
 /* Alerts */
 
 .alerts-container {
-    width: 448px;
+    width: 520px;
     z-index: $z-index-alerts;
     left: 0;
     right: 0;
diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx
index 2175db56429d8aa1e214a2d1bd01ae0dcfc54429..864c4848fd774a32c9bd712fb2d0883d185b47aa 100644
--- a/src/components/language-selector/language-selector.jsx
+++ b/src/components/language-selector/language-selector.jsx
@@ -7,11 +7,10 @@ import styles from './language-selector.css';
 // supported languages to exclude from the menu, but allow as a URL option
 const ignore = [];
 
-const LanguageSelector = ({componentRef, currentLocale, label, onChange}) => (
+const LanguageSelector = ({currentLocale, label, onChange}) => (
     <select
         aria-label={label}
         className={styles.languageSelect}
-        ref={componentRef}
         value={currentLocale}
         onChange={onChange}
     >
@@ -31,7 +30,6 @@ const LanguageSelector = ({componentRef, currentLocale, label, onChange}) => (
 );
 
 LanguageSelector.propTypes = {
-    componentRef: PropTypes.func,
     currentLocale: PropTypes.string,
     label: PropTypes.string,
     onChange: PropTypes.func
diff --git a/src/components/menu-bar/login-dropdown.jsx b/src/components/menu-bar/login-dropdown.jsx
index 11d23856c8f35edbec895c3b311073b8cc7ec743..0116d7342dfff258c1b8205aac36eb182bf381c6 100644
--- a/src/components/menu-bar/login-dropdown.jsx
+++ b/src/components/menu-bar/login-dropdown.jsx
@@ -7,11 +7,38 @@ eventually be consolidated.
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import React from 'react';
+import {defineMessages} from 'react-intl';
 
 import MenuBarMenu from './menu-bar-menu.jsx';
 
 import styles from './login-dropdown.css';
 
+// these are here as a hack to get them translated, so that equivalent messages will be translated
+// when passed in from www via gui's renderLogin() function
+const LoginDropdownMessages = defineMessages({ // eslint-disable-line no-unused-vars
+    username: {
+        defaultMessage: 'Username',
+        description: 'Label for login username input',
+        id: 'general.username'
+    },
+    password: {
+        defaultMessage: 'Password',
+        description: 'Label for login password input',
+        id: 'general.password'
+    },
+    signin: {
+        defaultMessage: 'Sign in',
+        description: 'Button text for user to sign in',
+        id: 'general.signIn'
+    },
+    needhelp: {
+        defaultMessage: 'Need Help?',
+        description: 'Button text for user to indicate that they need help',
+        id: 'login.needHelp'
+    }
+});
+
+
 const LoginDropdown = ({
     className,
     isOpen,
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index f7af33ec0bf7eeda60b8d757e2870a71e99d3229..87af1e683916e3a26d886a5bf14f6a2c9d0cb279 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -150,12 +150,13 @@ class MenuBar extends React.Component {
         }
     }
     handleClickNew () {
-        // if canCreateNew===true, it's safe to replace current project, since we will auto-save first.
-        // else confirm first.
-        const readyToReplaceProject = this.props.canCreateNew ||
+        // if canSave===true and canCreateNew===true, it's safe to replace current project,
+        // since we will auto-save first. Else, confirm first.
+        const readyToReplaceProject = (this.props.canSave && this.props.canCreateNew) ||
             confirm('Replace contents of the current project?'); // eslint-disable-line no-alert
+        this.props.onRequestCloseFile();
         if (readyToReplaceProject) {
-            this.props.onClickNew(this.props.canCreateNew);
+            this.props.onClickNew(this.props.canSave && this.props.canCreateNew);
         }
     }
     handleClickRemix () {
@@ -337,37 +338,25 @@ class MenuBar extends React.Component {
                                 place={this.props.isRtl ? 'left' : 'right'}
                                 onRequestClose={this.props.onRequestCloseFile}
                             >
-                                <MenuItem
-                                    isRtl={this.props.isRtl}
-                                    onClick={this.handleClickNew}
-                                >
-                                    {newProjectMessage}
-                                </MenuItem>
+                                <MenuSection>
+                                    <MenuItem
+                                        isRtl={this.props.isRtl}
+                                        onClick={this.handleClickNew}
+                                    >
+                                        {newProjectMessage}
+                                    </MenuItem>
+                                </MenuSection>
                                 <MenuSection>
                                     {this.props.canSave ? (
                                         <MenuItem onClick={this.handleClickSave}>
                                             {saveNowMessage}
                                         </MenuItem>
-                                    ) : (this.props.showComingSoon ? (
-                                        <MenuItemTooltip
-                                            id="save"
-                                            isRtl={this.props.isRtl}
-                                        >
-                                            <MenuItem>{saveNowMessage}</MenuItem>
-                                        </MenuItemTooltip>
-                                    ) : [])}
+                                    ) : []}
                                     {this.props.canCreateCopy ? (
                                         <MenuItem onClick={this.handleClickSaveAsCopy}>
                                             {createCopyMessage}
                                         </MenuItem>
-                                    ) : (this.props.showComingSoon ? (
-                                        <MenuItemTooltip
-                                            id="copy"
-                                            isRtl={this.props.isRtl}
-                                        >
-                                            <MenuItem>{createCopyMessage}</MenuItem>
-                                        </MenuItemTooltip>
-                                    ) : [])}
+                                    ) : []}
                                     {this.props.canRemix ? (
                                         <MenuItem onClick={this.handleClickRemix}>
                                             {remixMessage}
@@ -376,8 +365,9 @@ class MenuBar extends React.Component {
                                 </MenuSection>
                                 <MenuSection>
                                     <SBFileUploader onUpdateProjectTitle={this.props.onUpdateProjectTitle}>
-                                        {(renderFileInput, loadProject) => (
+                                        {(className, renderFileInput, loadProject) => (
                                             <MenuItem
+                                                className={className}
                                                 onClick={loadProject}
                                             >
                                                 <FormattedMessage
@@ -391,8 +381,9 @@ class MenuBar extends React.Component {
                                             </MenuItem>
                                         )}
                                     </SBFileUploader>
-                                    <SB3Downloader>{downloadProject => (
+                                    <SB3Downloader>{(className, downloadProject) => (
                                         <MenuItem
+                                            className={className}
                                             onClick={this.handleCloseFileMenuAndThen(downloadProject)}
                                         >
                                             <FormattedMessage
@@ -752,7 +743,7 @@ const mapDispatchToProps = dispatch => ({
     onRequestCloseLanguage: () => dispatch(closeLanguageMenu()),
     onClickLogin: () => dispatch(openLoginMenu()),
     onRequestCloseLogin: () => dispatch(closeLoginMenu()),
-    onClickNew: canCreateNew => dispatch(requestNewProject(canCreateNew)),
+    onClickNew: needSave => dispatch(requestNewProject(needSave)),
     onClickRemix: () => dispatch(remixProject()),
     onClickSave: () => dispatch(updateProject()),
     onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
diff --git a/src/components/menu/menu.jsx b/src/components/menu/menu.jsx
index 4e6a6b229170d5d8f0a7844c3fa58ff3425972bc..e69068052cb4e3b3027c3550bada15c1f0812e80 100644
--- a/src/components/menu/menu.jsx
+++ b/src/components/menu/menu.jsx
@@ -55,9 +55,10 @@ MenuItem.propTypes = {
 
 const addDividerClassToFirstChild = (child, id) => (
     React.cloneElement(child, {
-        className: classNames(child.className, {
-            [styles.menuSection]: id === 0
-        }),
+        className: classNames(
+            child.className,
+            {[styles.menuSection]: id === 0}
+        ),
         key: id
     })
 );
diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css
new file mode 100644
index 0000000000000000000000000000000000000000..89d22534859ecdd0a82f5686f5fcbe38103387b0
--- /dev/null
+++ b/src/components/spinner/spinner.css
@@ -0,0 +1,54 @@
+@import "../../css/colors.css";
+
+.spinner {
+    width: 1rem;
+    height: 1rem;
+    display: inline-block;
+    position: relative;
+    border-radius: 50%;
+    border-width: .1875rem;
+    border-style: solid;
+    border-color: $ui-white-transparent;
+}
+
+.spinner::after, .spinner::before {
+    width: .625rem;
+    height: .625rem;
+    content: '';
+    border-radius: 50%;
+    display: block;
+}
+
+.spinner::after {
+    position: absolute;
+    top: -.1875rem;
+    left: -.1875rem;
+    border: .1875rem solid transparent;
+    border-top-color: $ui-white;
+    animation: spin 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.spinner.orange {
+    border-color: $error-transparent;
+}
+
+.spinner.orange::after {
+    border-top-color: $error-primary;
+}
+
+.spinner.white {
+    border-color: $ui-white-transparent;
+}
+.spinner.white::after {
+    border-top-color: $ui-white;
+}
diff --git a/src/components/spinner/spinner.jsx b/src/components/spinner/spinner.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..08685ccb315220d25d1dd90f81b541f574808722
--- /dev/null
+++ b/src/components/spinner/spinner.jsx
@@ -0,0 +1,26 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import styles from './spinner.css';
+
+const SpinnerComponent = function (props) {
+    const {
+        className
+    } = props;
+    return (
+        <div
+            className={classNames(
+                styles.spinner,
+                className
+            )}
+        />
+    );
+};
+SpinnerComponent.propTypes = {
+    className: PropTypes.string
+};
+SpinnerComponent.defaultProps = {
+    className: ''
+};
+export default SpinnerComponent;
diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css
index d29d15c82c0777cf8eecec14ae027972715cc506..7a2c1f4cea61ece8df041a7ae9bb05502a8d4652 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.css
+++ b/src/components/sprite-selector-item/sprite-selector-item.css
@@ -42,6 +42,7 @@
 .sprite-image {
     margin: auto;
     user-select: none;
+    pointer-events: none;
     max-width: 32px;
     max-height: 32px;
 }
@@ -80,7 +81,7 @@
 .delete-button {
     position: absolute;
     top: 0.125rem;
-    z-index: 1;
+    z-index: auto;
 }
 
 [dir="ltr"] .delete-button {
diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index c10362a44797f876322f4c133290c16b0c712154..17ba2dc215345c14ddc3f4291ca7cc2700927e20 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -57,7 +57,8 @@ const SpriteList = function (props) {
                     DragConstants.COSTUME,
                     DragConstants.SOUND,
                     DragConstants.BACKPACK_COSTUME,
-                    DragConstants.BACKPACK_SOUND].includes(draggingType);
+                    DragConstants.BACKPACK_SOUND,
+                    DragConstants.BACKPACK_CODE].includes(draggingType);
 
                 return (
                     <SortableAsset
diff --git a/src/containers/alert.jsx b/src/containers/alert.jsx
index cb38427ad751c6a73b85a2da7ee0565e9ecec21d..62ff58448a03c172687a9a3c5518b516b3f0b187 100644
--- a/src/containers/alert.jsx
+++ b/src/containers/alert.jsx
@@ -1,40 +1,68 @@
 import React from 'react';
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
 
 import AlertComponent from '../components/alerts/alert.jsx';
+import {openConnectionModal} from '../reducers/modals';
+import {setConnectionModalExtensionId} from '../reducers/connection-modal';
 
 class Alert extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
-            'handleOnCloseAlert'
+            'handleOnCloseAlert',
+            'handleOnReconnect'
         ]);
     }
     handleOnCloseAlert () {
         this.props.onCloseAlert(this.props.index);
     }
+    handleOnReconnect () {
+        this.props.onOpenConnectionModal(this.props.extensionId);
+        this.handleOnCloseAlert();
+    }
     render () {
         const {
             index, // eslint-disable-line no-unused-vars
             iconURL,
-            message
+            message,
+            showReconnect
         } = this.props;
         return (
             <AlertComponent
                 iconURL={iconURL}
                 message={message}
+                showReconnect={showReconnect}
                 onCloseAlert={this.handleOnCloseAlert}
+                onReconnect={this.handleOnReconnect}
             />
         );
     }
 }
 
+const mapStateToProps = state => ({
+    state: state
+});
+
+const mapDispatchToProps = dispatch => ({
+    onOpenConnectionModal: id => {
+        dispatch(setConnectionModalExtensionId(id));
+        dispatch(openConnectionModal());
+    }
+});
+
 Alert.propTypes = {
+    extensionId: PropTypes.string,
     iconURL: PropTypes.string,
     index: PropTypes.number,
     message: PropTypes.string,
-    onCloseAlert: PropTypes.func.isRequired
+    onCloseAlert: PropTypes.func.isRequired,
+    onOpenConnectionModal: PropTypes.func,
+    showReconnect: PropTypes.bool.isRequired
 };
 
-export default Alert;
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(Alert);
diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx
index 32c6fea0025f06bdb153ff61b889c31a9d04eb0f..1a22a185e956db246d371aa8e1a10eb5ca883e6c 100644
--- a/src/containers/backpack.jsx
+++ b/src/containers/backpack.jsx
@@ -29,11 +29,12 @@ class Backpack extends React.Component {
             'handleToggle',
             'handleDelete',
             'getBackpackAssetURL',
-            'refreshContents',
+            'getContents',
             'handleMouseEnter',
             'handleMouseLeave',
             'handleBlockDragEnd',
-            'handleBlockDragUpdate'
+            'handleBlockDragUpdate',
+            'handleMore'
         ]);
         this.state = {
             // While the DroppableHOC manages drop interactions for asset tiles,
@@ -42,8 +43,8 @@ class Backpack extends React.Component {
             blockDragOutsideWorkspace: false,
             blockDragOverBackpack: false,
             error: false,
-            offset: 0,
             itemsPerPage: 20,
+            moreToLoad: false,
             loading: false,
             expanded: false,
             contents: []
@@ -72,12 +73,12 @@ class Backpack extends React.Component {
     }
     handleToggle () {
         const newState = !this.state.expanded;
-        this.setState({expanded: newState, offset: 0}, () => {
+        this.setState({expanded: newState, contents: []}, () => {
             // Emit resize on window to get blocks to resize
             window.dispatchEvent(new Event('resize'));
         });
         if (newState) {
-            this.refreshContents();
+            this.getContents();
         }
     }
     handleDrop (dragInfo) {
@@ -107,33 +108,60 @@ class Backpack extends React.Component {
                     username: this.props.username,
                     ...payload
                 }))
-                .then(this.refreshContents);
+                .then(item => {
+                    this.setState({
+                        loading: false,
+                        contents: [item].concat(this.state.contents)
+                    });
+                })
+                .catch(error => {
+                    this.setState({error: true, loading: false});
+                    throw error;
+                });
         });
     }
     handleDelete (id) {
-        deleteBackpackObject({
-            host: this.props.host,
-            token: this.props.token,
-            username: this.props.username,
-            id: id
-        }).then(this.refreshContents);
-    }
-    refreshContents () {
-        if (this.props.token && this.props.username) {
-            this.setState({loading: true, error: false});
-            getBackpackContents({
+        this.setState({loading: true}, () => {
+            deleteBackpackObject({
                 host: this.props.host,
                 token: this.props.token,
                 username: this.props.username,
-                offset: this.state.offset,
-                limit: this.state.itemsPerPage
+                id: id
             })
-                .then(contents => {
-                    this.setState({contents, loading: false});
+                .then(() => {
+                    this.setState({
+                        loading: false,
+                        contents: this.state.contents.filter(o => o.id !== id)
+                    });
                 })
-                .catch(() => {
+                .catch(error => {
                     this.setState({error: true, loading: false});
+                    throw error;
                 });
+        });
+    }
+    getContents () {
+        if (this.props.token && this.props.username) {
+            this.setState({loading: true, error: false}, () => {
+                getBackpackContents({
+                    host: this.props.host,
+                    token: this.props.token,
+                    username: this.props.username,
+                    offset: this.state.contents.length,
+                    limit: this.state.itemsPerPage
+                })
+                    .then(contents => {
+                        this.setState({
+                            contents: this.state.contents.concat(contents),
+                            moreToLoad: contents.length === this.state.itemsPerPage,
+                            loading: false
+                        });
+                    })
+                    .catch(error => {
+                        this.setState({error: true, loading: false});
+                        throw error;
+                    });
+            });
         }
     }
     handleBlockDragUpdate (isOutsideWorkspace) {
@@ -168,6 +196,9 @@ class Backpack extends React.Component {
             blockDragOutsideWorkspace: false
         });
     }
+    handleMore () {
+        this.getContents();
+    }
     render () {
         return (
             <DroppableBackpack
@@ -176,8 +207,10 @@ class Backpack extends React.Component {
                 error={this.state.error}
                 expanded={this.state.expanded}
                 loading={this.state.loading}
+                showMore={this.state.moreToLoad}
                 onDelete={this.handleDelete}
                 onDrop={this.handleDrop}
+                onMore={this.handleMore}
                 onMouseEnter={this.handleMouseEnter}
                 onMouseLeave={this.handleMouseLeave}
                 onToggle={this.props.host ? this.handleToggle : null}
diff --git a/src/containers/language-selector.jsx b/src/containers/language-selector.jsx
index fe24812bd82892621673308f866ee9b6486686a7..1672949a3716ea11b88a71b5e53dbf389886af24 100644
--- a/src/containers/language-selector.jsx
+++ b/src/containers/language-selector.jsx
@@ -11,31 +11,10 @@ class LanguageSelector extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
-            'handleChange',
-            'handleMouseOut',
-            'ref'
+            'handleChange'
         ]);
         document.documentElement.lang = props.currentLocale;
     }
-    componentDidMount () {
-        this.addListeners();
-    }
-
-    componentWillUnmount () {
-        this.removeListeners();
-    }
-    addListeners () {
-        this.select.addEventListener('mouseout', this.handleMouseOut);
-    }
-    removeListeners () {
-        this.select.removeEventListener('mouseout', this.handleMouseOut);
-    }
-    handleMouseOut () {
-        this.select.blur();
-    }
-    ref (c) {
-        this.select = c;
-    }
     handleChange (e) {
         const newLocale = e.target.value;
         if (this.props.messagesByLocale[newLocale]) {
@@ -52,7 +31,6 @@ class LanguageSelector extends React.Component {
         } = this.props;
         return (
             <LanguageSelectorComponent
-                componentRef={this.ref}
                 onChange={this.handleChange}
                 {...props}
             >
diff --git a/src/containers/sb-file-uploader.jsx b/src/containers/sb-file-uploader.jsx
index ffa4e0122b8cd12265a573cb60050c2f8eaef973..f7bacfd7391db72a85483840f977bfe12c507c6a 100644
--- a/src/containers/sb-file-uploader.jsx
+++ b/src/containers/sb-file-uploader.jsx
@@ -107,13 +107,14 @@ class SBFileUploader extends React.Component {
         );
     }
     render () {
-        return this.props.children(this.renderFileInput, this.handleClick);
+        return this.props.children(this.props.className, this.renderFileInput, this.handleClick);
     }
 }
 
 SBFileUploader.propTypes = {
     canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
     children: PropTypes.func,
+    className: PropTypes.string,
     intl: intlShape.isRequired,
     loadingState: PropTypes.oneOf(LoadingStates),
     onLoadingFinished: PropTypes.func,
@@ -123,6 +124,9 @@ SBFileUploader.propTypes = {
         loadProject: PropTypes.func
     })
 };
+SBFileUploader.defaultProps = {
+    className: ''
+};
 const mapStateToProps = state => ({
     loadingState: state.scratchGui.projectState.loadingState,
     vm: state.scratchGui.vm
diff --git a/src/containers/sb3-downloader.jsx b/src/containers/sb3-downloader.jsx
index 703674c62398a0a71a2424118d9155e556583c01..47df7bacaebe341abc571d3c9ca7e22e2d024e34 100644
--- a/src/containers/sb3-downloader.jsx
+++ b/src/containers/sb3-downloader.jsx
@@ -52,6 +52,7 @@ class SB3Downloader extends React.Component {
             children
         } = this.props;
         return children(
+            this.props.className,
             this.downloadProject
         );
     }
@@ -67,10 +68,14 @@ const getProjectFilename = (curTitle, defaultTitle) => {
 
 SB3Downloader.propTypes = {
     children: PropTypes.func,
+    className: PropTypes.string,
     onSaveFinished: PropTypes.func,
     projectFilename: PropTypes.string,
     saveProjectSb3: PropTypes.func
 };
+SB3Downloader.defaultProps = {
+    className: ''
+};
 
 const mapStateToProps = state => ({
     saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm),
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index c448df486b5861c998e7094e89ea55c12bc74fe3..f284be34e1990ee13936d1dfdc6cbcc204433faf 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -119,7 +119,7 @@ class SpriteSelectorItem extends React.Component {
     render () {
         const {
             /* eslint-disable no-unused-vars */
-            assetId,
+            asset,
             id,
             index,
             onClick,
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
index e8f0fc9df0b2fefbb31f1c18ba52bc56f48bdc2f..08ffd3730a520da9a16a92af68fde070d8e3a1ee 100644
--- a/src/containers/stage-selector.jsx
+++ b/src/containers/stage-selector.jsx
@@ -12,6 +12,7 @@ import DragConstants from '../lib/drag-constants';
 import DropAreaHOC from '../lib/drop-area-hoc.jsx';
 import {emptyCostume} from '../lib/empty-assets';
 import sharedMessages from '../lib/shared-messages';
+import {fetchCode} from '../lib/backpack-api';
 
 import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
 
@@ -22,7 +23,8 @@ const dragTypes = [
     DragConstants.COSTUME,
     DragConstants.SOUND,
     DragConstants.BACKPACK_COSTUME,
-    DragConstants.BACKPACK_SOUND
+    DragConstants.BACKPACK_SOUND,
+    DragConstants.BACKPACK_CODE
 ];
 
 const DroppableStage = DropAreaHOC(dragTypes)(StageSelectorComponent);
@@ -99,6 +101,12 @@ class StageSelector extends React.Component {
                 md5: dragInfo.payload.body,
                 name: dragInfo.payload.name
             }, this.props.id);
+        } else if (dragInfo.dragType === DragConstants.BACKPACK_CODE) {
+            fetchCode(dragInfo.payload.bodyUrl)
+                .then(blocks => {
+                    this.props.vm.shareBlocksToTarget(blocks, this.props.id);
+                    this.props.vm.refreshWorkspace();
+                });
         }
     }
     setFileInput (input) {
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index cde8d4af41f8feb9d02b9ac101b53674b01e92c1..b48e3d232acd59d6a7428b3df89aa3e4404cd594 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -19,6 +19,7 @@ import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js';
 import sharedMessages from '../lib/shared-messages';
 import {emptySprite} from '../lib/empty-assets';
 import {highlightTarget} from '../reducers/targets';
+import {fetchSprite, fetchCode} from '../lib/backpack-api';
 
 class TargetPane extends React.Component {
     constructor (props) {
@@ -166,8 +167,7 @@ class TargetPane extends React.Component {
         } else if (dragInfo.dragType === DragConstants.BACKPACK_SPRITE) {
             // TODO storage does not have a way of loading zips right now, and may never need it.
             // So for now just grab the zip manually.
-            fetch(dragInfo.payload.bodyUrl)
-                .then(response => response.arrayBuffer())
+            fetchSprite(dragInfo.payload.bodyUrl)
                 .then(sprite3Zip => this.props.vm.addSprite(sprite3Zip));
         } else if (targetId) {
             // Something is being dragged over one of the sprite tiles or the backdrop.
@@ -192,6 +192,12 @@ class TargetPane extends React.Component {
                     md5: dragInfo.payload.body,
                     name: dragInfo.payload.name
                 }, targetId);
+            } else if (dragInfo.dragType === DragConstants.BACKPACK_CODE) {
+                fetchCode(dragInfo.payload.bodyUrl)
+                    .then(blocks => {
+                        this.props.vm.shareBlocksToTarget(blocks, targetId);
+                        this.props.vm.refreshWorkspace();
+                    });
             }
         }
     }
@@ -231,6 +237,7 @@ class TargetPane extends React.Component {
 
 const {
     onSelectSprite, // eslint-disable-line no-unused-vars
+    onActivateBlocksTab, // eslint-disable-line no-unused-vars
     ...targetPaneProps
 } = TargetPaneComponent.propTypes;
 
diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js
index 4a6f7f6b24ccf2e9fba256fbed97d28ccf5c8aa7..550d37d43d6bebfc7c329b4c49f62f38e2ad3490 100644
--- a/src/lib/backpack-api.js
+++ b/src/lib/backpack-api.js
@@ -4,6 +4,14 @@ import soundPayload from './backpack/sound-payload';
 import spritePayload from './backpack/sprite-payload';
 import codePayload from './backpack/code-payload';
 
+// Add a new property for the full thumbnail url, which includes the host.
+// Also include a full body url for loading sprite zips
+// TODO retreiving the images through storage would allow us to remove this.
+const includeFullUrls = (item, host) => Object.assign({}, item, {
+    thumbnailUrl: `${host}/${item.thumbnail}`,
+    bodyUrl: `${host}/${item.body}`
+});
+
 const getBackpackContents = ({
     host,
     username,
@@ -20,15 +28,7 @@ const getBackpackContents = ({
         if (error || response.statusCode !== 200) {
             return reject();
         }
-        // Add a new property for the full thumbnail url, which includes the host.
-        // Also include a full body url for loading sprite zips
-        // TODO retreiving the images through storage would allow us to remove this.
-        return resolve(response.body.map(item => (
-            Object.assign({}, item, {
-                thumbnailUrl: `${host}/${item.thumbnail}`,
-                bodyUrl: `${host}/${item.body}`
-            })
-        )));
+        return resolve(response.body.map(item => includeFullUrls(item, host)));
     });
 });
 
@@ -51,7 +51,7 @@ const saveBackpackObject = ({
         if (error || response.statusCode !== 200) {
             return reject();
         }
-        return resolve(response.body);
+        return resolve(includeFullUrls(response.body, host));
     });
 });
 
@@ -73,6 +73,22 @@ const deleteBackpackObject = ({
     });
 });
 
+// Two types of backpack items are not retreivable through storage
+// code, as json and sprite3 as arraybuffer zips.
+const fetchAs = (responseType, uri) => new Promise((resolve, reject) => {
+    xhr({uri, responseType}, (error, response) => {
+        if (error || response.statusCode !== 200) {
+            return reject();
+        }
+        return resolve(response.body);
+    });
+});
+
+// These two helpers allow easy fetching of backpack code and sprite zips
+// Use the curried fetchAs here so the consumer does not worry about XHR responseTypes
+const fetchCode = fetchAs.bind(null, 'json');
+const fetchSprite = fetchAs.bind(null, 'arraybuffer');
+
 export {
     getBackpackContents,
     saveBackpackObject,
@@ -80,5 +96,7 @@ export {
     costumePayload,
     soundPayload,
     spritePayload,
-    codePayload
+    codePayload,
+    fetchCode,
+    fetchSprite
 };
diff --git a/src/lib/backpack/block-to-image.js b/src/lib/backpack/block-to-image.js
index 8c38d29bca3eb0d662e924e80ee26f367d9a440a..eac4753b3c914f33541a675055ff2da481440770 100644
--- a/src/lib/backpack/block-to-image.js
+++ b/src/lib/backpack/block-to-image.js
@@ -15,6 +15,9 @@ export default function (blockId) {
     // blocking the drag end from finishing promptly.
     return new Promise(resolve => {
         setTimeout(() => {
+            // Strip &nbsp; entities that cannot be inlined
+            blockSvg.innerHTML = blockSvg.innerHTML.replace(/&nbsp;/g, ' ');
+
             // Create an <svg> element to put the cloned blockSvg inside
             const NS = 'http://www.w3.org/2000/svg';
             const svg = document.createElementNS(NS, 'svg');
diff --git a/src/lib/backpack/jpeg-thumbnail.js b/src/lib/backpack/jpeg-thumbnail.js
index 623daf3befdefb74a18b9ab98337484a5691c829..f0fbd28d25fe9406d8c111bba2849b4df3c53e0a 100644
--- a/src/lib/backpack/jpeg-thumbnail.js
+++ b/src/lib/backpack/jpeg-thumbnail.js
@@ -1,4 +1,4 @@
-const jpegThumbnail = dataUrl => new Promise(resolve => {
+const jpegThumbnail = dataUrl => new Promise((resolve, reject) => {
     const image = new Image();
     image.onload = () => {
         const canvas = document.createElement('canvas');
@@ -14,6 +14,9 @@ const jpegThumbnail = dataUrl => new Promise(resolve => {
         // TODO we can play with the `quality` option here to optimize file size
         resolve(canvas.toDataURL('image/jpeg', 0.92 /* quality */)); // Default quality is 0.92
     };
+    image.onerror = err => {
+        reject(err);
+    };
     image.src = dataUrl;
 });
 
diff --git a/src/lib/blocks.js b/src/lib/blocks.js
index 57283adbbc7033a20ab286719ce5b0b230df51a4..212831d0aaab7af48c4a76300403438923d9fa6e 100644
--- a/src/lib/blocks.js
+++ b/src/lib/blocks.js
@@ -220,5 +220,9 @@ export default function (vm) {
         return ScratchBlocks.StatusButtonState.NOT_READY;
     };
 
+    ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) {
+        vm.runtime.emit('PLAY_NOTE', noteNum, extensionId);
+    };
+
     return ScratchBlocks;
 }
diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js
index ff3d34c40bc8472e4652d037fa09e82bb211bb73..9ca5424e3fb1941b94ce7bb603a76b99680afe79 100644
--- a/src/lib/cloud-provider.js
+++ b/src/lib/cloud-provider.js
@@ -79,11 +79,14 @@ class CloudProvider {
         msg.user = this.username;
         msg.project_id = this.projectId;
 
+        // Optional string params can use simple falsey undefined check
         if (dataName) msg.name = dataName;
-        if (dataValue) msg.value = dataValue;
-        if (dataIndex) msg.index = dataIndex;
         if (dataNewName) msg.new_name = dataNewName;
 
+        // Optional number params need different undefined check
+        if (typeof dataValue !== 'undefined') msg.value = dataValue;
+        if (typeof dataValue !== 'undefined') msg.index = dataIndex;
+
         const dataToWrite = JSON.stringify(msg);
         this.sendCloudData(dataToWrite);
     }
diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 92748b20f61af930ede35531be858ead618d5b16..936859666aff40e8c4749299b753562ea7cadbdd 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -312,7 +312,7 @@ const sound = function (isStage, targetId) {
                 </shadow>
             </value>
         </block>
-        <block id="volume" type="sound_volume"/>
+        <block id="${targetId}_volume" type="sound_volume"/>
         ${categorySeparator}
     </category>
     `;
diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js
index fefc97b37327eb2310dfa1b430afe1bfccf992cb..ee5ef0931596878c9c0cf59102921848fb2f8873 100644
--- a/src/reducers/alerts.js
+++ b/src/reducers/alerts.js
@@ -15,11 +15,14 @@ const reducer = function (state, action) {
         const newList = state.alertsList.slice();
         const newAlert = {message: action.data.message};
         const extensionId = action.data.extensionId;
+        newAlert.showReconnect = false;
         if (extensionId) { // if it's an extension
             const extension = extensionData.find(ext => ext.extensionId === extensionId);
             if (extension && extension.name) {
                 // TODO: is this the right place to assemble this message?
+                newAlert.extensionId = extensionId;
                 newAlert.message = `${newAlert.message} ${extension.name}.`;
+                newAlert.showReconnect = true;
             }
             if (extension && extension.smallPeripheralImage) {
                 newAlert.iconURL = extension.smallPeripheralImage;
diff --git a/src/reducers/project-state.js b/src/reducers/project-state.js
index 963ea9ec110f9538bd2e52eb89ac4e7bea5b7951..e264f18a48079260c7763471aebd51035ac03d51 100644
--- a/src/reducers/project-state.js
+++ b/src/reducers/project-state.js
@@ -379,8 +379,8 @@ const setProjectId = id => ({
     projectId: id
 });
 
-const requestNewProject = canCreateNew => {
-    if (canCreateNew) return {type: START_UPDATING_BEFORE_CREATING_NEW};
+const requestNewProject = needSave => {
+    if (needSave) return {type: START_UPDATING_BEFORE_CREATING_NEW};
     return {type: START_FETCHING_NEW};
 };
 
diff --git a/test/fixtures/100-100.svg b/test/fixtures/100-100.svg
new file mode 100644
index 0000000000000000000000000000000000000000..108f7aa1bc9c4c6cd0d5b4a4728ae0261d5342b7
Binary files /dev/null and b/test/fixtures/100-100.svg differ
diff --git a/test/fixtures/gh-3582-png.png b/test/fixtures/gh-3582-png.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc1f1464e957dbddf5bc1aa4b8915b988bc97f0f
Binary files /dev/null and b/test/fixtures/gh-3582-png.png differ
diff --git a/test/integration/connection-modal.test.js b/test/integration/connection-modal.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..117be5ce5ed7031092fb40b8a120b68074d9cfcc
--- /dev/null
+++ b/test/integration/connection-modal.test.js
@@ -0,0 +1,66 @@
+import path from 'path';
+import SeleniumHelper from '../helpers/selenium-helper';
+
+const {
+    clickText,
+    clickXpath,
+    findByText,
+    getDriver,
+    getLogs,
+    loadUri
+} = new SeleniumHelper();
+
+const uri = path.resolve(__dirname, '../../build/index.html');
+
+let driver;
+
+// The tests below require Scratch Link to be unavailable, so we can trigger
+// an error modal. To make sure this is always true, we came up with the idea of
+// injecting javascript that overwrites the global Websocket object with one that
+// attempts to connect to a fake socket address.
+const websocketFakeoutJs = `var RealWebSocket = WebSocket;
+    WebSocket = function () {
+        return new RealWebSocket("wss://fake.fake");
+    }`;
+
+describe('Hardware extension connection modal', () => {
+    beforeAll(() => {
+        driver = getDriver();
+    });
+
+    afterAll(async () => {
+        await driver.quit();
+    });
+
+    test('Message saying Scratch Link is unavailable (BLE)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+
+        await driver.executeScript(websocketFakeoutJs);
+
+        await clickXpath('//button[@title="Add Extension"]');
+
+        await clickText('micro:bit');
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open
+        findByText('Scratch Link'); // Scratch Link is mentioned in the error modal
+
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    test('Message saying Scratch Link is unavailable (BT)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+
+        await driver.executeScript(websocketFakeoutJs);
+
+        await clickXpath('//button[@title="Add Extension"]');
+
+        await clickText('EV3');
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open
+        findByText('Scratch Link'); // Scratch Link is mentioned in the error modal
+
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+});
diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js
index 7aa3b969fc2e152cd461c1c649f392e5fc00dfc9..78c7b911e191c316c0c9b0bd4aa4439edafc32f5 100644
--- a/test/integration/costumes.test.js
+++ b/test/integration/costumes.test.js
@@ -129,4 +129,35 @@ describe('Working with costumes', () => {
         const logs = await getLogs();
         await expect(logs).toEqual([]);
     });
+
+    test('Adding an svg from file', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        await clickText('Costumes');
+        const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg'));
+        await clickText('100-100', scope.costumesTab); // Name from filename
+        await clickText('100 x 100', scope.costumesTab); // Size is right
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    test('Adding a png from file (gh-3582)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        await clickText('Costumes');
+        const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png'));
+        await clickText('gh-3582-png', scope.costumesTab);
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
 });
diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js
index a5c4ede2b7e2b2d8d932d34bb1f257013bdf38b6..ca1a4308b7a97c72a1f824e559b00a1e64859722 100644
--- a/test/integration/menu-bar.test.js
+++ b/test/integration/menu-bar.test.js
@@ -31,25 +31,24 @@ describe('Menu bar settings', () => {
         await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]');
     });
 
-    test('File->Save now should NOT be enabled', async () => {
+    test('File->Upload should be enabled', async () => {
         await loadUri(uri);
         await clickXpath('//button[@title="Try It"]');
         await clickXpath(
             '//div[contains(@class, "menu-bar_menu-bar-item") and ' +
             'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
         );
-        await findByXpath('//*[li[span[text()="Save now"]] and @data-tip="tooltip"]');
+        await findByXpath('//*[li[span[text()="Upload from your computer"]] and not(@data-tip="tooltip")]');
     });
 
-
-    test('File->Save as a copy should NOT be enabled', async () => {
+    test('File->Download should be enabled', async () => {
         await loadUri(uri);
         await clickXpath('//button[@title="Try It"]');
         await clickXpath(
             '//div[contains(@class, "menu-bar_menu-bar-item") and ' +
             'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
         );
-        await findByXpath('//*[li[span[text()="Save as a copy"]] and @data-tip="tooltip"]');
+        await findByXpath('//*[li[span[text()="Download to your computer"]] and not(@data-tip="tooltip")]');
     });
 
     test('Share button should NOT be enabled', async () => {
diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js
index 83a07793dc7264c4a555c5d6d017802806003eaa..ca6a84ebaca6bdb7f5de3938e5410495e12023f6 100644
--- a/test/integration/sprites.test.js
+++ b/test/integration/sprites.test.js
@@ -74,4 +74,39 @@ describe('Working with sprites', () => {
         const logs = await getLogs();
         await expect(logs).toEqual([]);
     });
+
+    test('Adding a sprite by uploading a png', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png'));
+        await clickText('gh-3582-png', scope.spriteTile);
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    // This test fails because uploading an SVG as a sprite changes the scaling
+    // Enable when this is fixed issues/3608
+    test.skip('Adding a sprite by uploading an svg (gh-3608)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg'));
+        await clickText('100-100', scope.spriteTile); // Sprite is named for costume filename
+
+        // Check to make sure the size is right
+        await clickText('Costumes');
+        await clickText('100-100-costume1', scope.costumesTab); // The name of the costume
+        await clickText('100 x 100', scope.costumesTab); // The size of the costume
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
 });
diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..65ebbc06e4b50d36c6e111e6cea83ccf3679c795
--- /dev/null
+++ b/test/unit/util/cloud-provider.test.js
@@ -0,0 +1,44 @@
+import CloudProvider from '../../../src/lib/cloud-provider';
+
+// Disable window.WebSocket
+global.WebSocket = null;
+
+describe('CloudProvider', () => {
+    let cloudProvider = null;
+    let sentMessage = null;
+
+    beforeEach(() => {
+        cloudProvider = new CloudProvider();
+        // Stub connection
+        cloudProvider.connection = {
+            send: msg => {
+                sentMessage = msg;
+            }
+        };
+    });
+
+    test('updateVariable', () => {
+        cloudProvider.updateVariable('hello', 1);
+        const obj = JSON.parse(sentMessage);
+        expect(obj.method).toEqual('set');
+        expect(obj.name).toEqual('hello');
+        expect(obj.value).toEqual(1);
+    });
+
+    test('updateVariable with falsey value', () => {
+        cloudProvider.updateVariable('hello', 0);
+        const obj = JSON.parse(sentMessage);
+        expect(obj.method).toEqual('set');
+        expect(obj.name).toEqual('hello');
+        expect(obj.value).toEqual(0);
+    });
+
+    test('writeToServer with falsey index value', () => {
+        cloudProvider.writeToServer('method', 'name', 5, 0);
+        const obj = JSON.parse(sentMessage);
+        expect(obj.method).toEqual('method');
+        expect(obj.name).toEqual('name');
+        expect(obj.value).toEqual(5);
+        expect(obj.index).toEqual(0);
+    });
+});