diff --git a/src/components/close-button/close-button.css b/src/components/close-button/close-button.css index 1864e7b2aaede98cf6f4b827db6ba63586275f47..88ebb526ef01c81aeacae0597450e5c992b88951 100644 --- a/src/components/close-button/close-button.css +++ b/src/components/close-button/close-button.css @@ -49,3 +49,18 @@ width: 0.75rem; height: 0.75rem; } + +.back-icon { + position: relative; + margin: 0.25rem; + user-select: none; +} + +.small .back-icon { + width: 50%; +} + +.large .back-icon { + width: 2rem; + height: 2rem; +} diff --git a/src/components/close-button/close-button.jsx b/src/components/close-button/close-button.jsx index 039a8750b7ae108506a2077c07ad2e66496c1217..befac229f5b0a5da58f3c1e1b25b6d9d64c79357 100644 --- a/src/components/close-button/close-button.jsx +++ b/src/components/close-button/close-button.jsx @@ -4,6 +4,7 @@ import classNames from 'classnames'; import styles from './close-button.css'; import closeIcon from './icon--close.svg'; +import backIcon from './icon--back.svg'; const CloseButton = props => ( <div @@ -20,10 +21,16 @@ const CloseButton = props => ( tabIndex="0" onClick={props.onClick} > - <img - className={styles.closeIcon} - src={closeIcon} - /> + {props.buttonType === 'back' ? + <img + className={styles.backIcon} + src={backIcon} + /> : + <img + className={styles.closeIcon} + src={closeIcon} + /> + } </div> ); @@ -31,13 +38,15 @@ CloseButton.SIZE_SMALL = 'small'; CloseButton.SIZE_LARGE = 'large'; CloseButton.propTypes = { + buttonType: PropTypes.oneOf(['back', 'close']), className: PropTypes.string, onClick: PropTypes.func.isRequired, size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE]) }; CloseButton.defaultProps = { - size: CloseButton.SIZE_LARGE + size: CloseButton.SIZE_LARGE, + buttonType: 'close' }; export default CloseButton; diff --git a/src/components/close-button/icon--back.svg b/src/components/close-button/icon--back.svg new file mode 100644 index 0000000000000000000000000000000000000000..e3f82a6fdb679c27301413397fc8838f74bbb9ae Binary files /dev/null and b/src/components/close-button/icon--back.svg differ diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 488e705e88db5ce7cb76a4cc320446b0dab1b35b..5c7a378dc43c44ff9f3d5d37e3b9ae79ffbbe87f 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -19,6 +19,7 @@ import Box from '../box/box.jsx'; import FeedbackForm from '../feedback-form/feedback-form.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx'; import PreviewModal from '../../containers/preview-modal.jsx'; +import ImportModal from '../../containers/import-modal.jsx'; import WebGlModal from '../../containers/webgl-modal.jsx'; import layout from '../../lib/layout-constants.js'; @@ -41,6 +42,7 @@ const GUIComponent = props => { children, costumesTabVisible, feedbackFormVisible, + importInfoVisible, intl, onExtensionButtonClick, onActivateTab, @@ -76,6 +78,9 @@ const GUIComponent = props => { {previewInfoVisible ? ( <PreviewModal /> ) : null} + {importInfoVisible ? ( + <ImportModal /> + ) : null} {feedbackFormVisible ? ( <FeedbackForm /> ) : null} @@ -169,9 +174,11 @@ GUIComponent.propTypes = { children: PropTypes.node, costumesTabVisible: PropTypes.bool, feedbackFormVisible: PropTypes.bool, + importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, onActivateTab: PropTypes.func, onExtensionButtonClick: PropTypes.func, + onTabSelect: PropTypes.func, previewInfoVisible: PropTypes.bool, soundsTabVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css new file mode 100644 index 0000000000000000000000000000000000000000..b794fd7530cf3711fdf2dae1891df79c3138324d --- /dev/null +++ b/src/components/import-modal/import-modal.css @@ -0,0 +1,185 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: hsla(215, 100%, 65%, .9); +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid hsla(0, 100%, 100%, .25); + padding: 0; + border-radius: $space; + user-select: none; + width: 500px; + color: $text-primary; + overflow: hidden; +} + +/* + TODO figure out how to remove filter altogether + since it is null... + Modal header has 3 items: + |x title filter| + + Use the same width for both side item containers, + so that title remains centered +*/ +$sides: 20rem; + +.header { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + height: $library-header-height; + + box-sizing: border-box; + width: 100%; + background-color: $import-primary; +} + +.header-item { + display: flex; + align-items: center; + padding: 1rem; + text-decoration: none; + color: white; + user-select: none; +} + +.header-item-filter { + display: flex; + flex-basis: $sides; + justify-content: flex-end; +} + +.header-item-title { + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + user-select: none; + letter-spacing: 0.4px; + cursor: default; +} + +.header-item-title h2 { + font-size: 1.25rem; +} + +.header-item-close { + display: flex; + flex-basis: $sides; + justify-content: flex-start; +} + +.body { + background: $ui-pane-gray; + padding: 1.5rem 2.25rem; + text-align: center; +} + +.input-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; + border: 1px solid; + border-radius: 0.25rem; + overflow: hidden; +} + +.ok-input-container { + border-color: $motion-primary; + box-shadow: 0 0 0 0.2rem $motion-transparent; +} + +.bad-input-container { + border-color: $data-primary; + box-shadow: 0 0 0 0.2rem hsla(30, 100%, 55%, 0.15); +} + +.input-row input { + width: 100%; + padding: 0 1rem; + height: 3rem; + color: #6b6b6b; + font-size: .875rem; + outline: none; + border: none; +} + +.input-row input::placeholder { + font-style: italic; + color: rgba(87,94,117,0.5); +} + +.input-row button { + padding: 0.5rem 2rem; + font-weight: bold; + font-size: .875rem; + cursor: pointer; + border: 0px solid $import-primary; + outline: none; +} + +.input-row button.ok-button { + background: $import-primary; + color: white; +} + +.error-row { + margin: 1.5rem 0; + text-align: center; + display: flex; + justify-content: center; + background: hsla(30, 100%, 55%, 0.25); + color: $data-primary; + border: 1px solid $data-primary; + border-radius: 0.25rem; +} + +.error-row p { + font-size: 0.875rem; + font-weight: bold; +} + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 1.5rem; + background: white; + font-weight: bold; + font-size: .875rem; + cursor: pointer; + color: $motion-primary; +} + +.faq-link-text { + margin: 2rem 0 .5rem 0; + font-size: .875rem; + color: $text-primary; +} + +.faq-link { + color: $motion-primary; + text-decoration: none; +} diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ad42929e7d6c24afced4c2febf2bda976ea27f2b --- /dev/null +++ b/src/components/import-modal/import-modal.jsx @@ -0,0 +1,155 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactModal from 'react-modal'; +import Box from '../box/box.jsx'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; + +import CloseButton from '../close-button/close-button.jsx'; + +import styles from './import-modal.css'; + +const messages = defineMessages({ + title: { + id: 'gui.importInfo.title', + defaultMessage: 'View a Scratch 2.0 Project', + description: 'Scratch 2.0 import modal label - for accessibility' + }, + formDescription: { + defaultMessage: + 'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved.', + description: 'Import project message', + id: 'gui.importInfo.message' + }, + invalidFormatError: { + id: 'gui.importInfo.invalidFormatError', + defaultMessage: 'Uh oh, that project link or id doesn\'t look quite right.', + description: 'Invalid project link or id message' + } +}); + +const ImportModal = ({intl, ...props}) => ( + <ReactModal + isOpen + className={styles.modalContent} + contentLabel={intl.formatMessage({...messages.title})} + overlayClassName={styles.modalOverlay} + onRequestClose={props.onCancel} + > + <Box> + <div className={styles.header}> + <div + className={classNames( + styles.headerItem, + styles.headerItemClose + )} + > + <CloseButton + buttonType="back" + size={CloseButton.SIZE_LARGE} + onClick={props.onGoBack} + /> + </div> + <div + className={classNames( + styles.headerItem, + styles.headerItemTitle + )} + > + <h2> + {intl.formatMessage({...messages.title})} + </h2> + </div> + <div className={classNames(styles.headerItem, styles.headerItemFilter)}> + {null} + </div> + </div> + </Box> + + <Box className={styles.body}> + <p> + {intl.formatMessage({...messages.formDescription})} + </p> + <Box + className={classNames(styles.inputRow, + (props.hasValidationError ? styles.badInputContainer : styles.okInputContainer)) + } + > + <input + autoFocus + placeholder={props.placeholder} + value={props.inputValue} + onChange={props.onChange} + onKeyPress={props.onKeyPress} + /> + <button + className={styles.okButton} + title="viewproject" + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View" + description="Label for button to load a scratch 2.0 project" + id="gui.importModal.viewproject" + /> + </button> + </Box> + {props.hasValidationError ? + <Box className={styles.errorRow}> + <p> + <FormattedMessage + {...messages[`${props.errorMessage}`]} + /> + </p> + </Box> : null + } + <Box className={styles.buttonRow}> + <button + onClick={props.onGoBack} + > + <FormattedMessage + defaultMessage="Go Back" + description="Label for button to back out of importing a project" + id="gui.importInfo.goback" + /> + </button> + </Box> + <Box className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.importInfo.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/preview-faq" + > + <FormattedMessage + defaultMessage="Preview FAQ" + description="link to Scratch 3.0 preview FAQ page" + id="gui.importInfo.previewfaqlink" + /> + </a> + ) + }} + /> + </Box> + </Box> + </ReactModal> +); + +ImportModal.propTypes = { + errorMessage: PropTypes.string.isRequired, + hasValidationError: PropTypes.bool.isRequired, + inputValue: PropTypes.string.isRequired, + intl: intlShape.isRequired, + onCancel: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onGoBack: PropTypes.func.isRequired, + onKeyPress: PropTypes.func.isRequired, + onViewProject: PropTypes.func.isRequired, + placeholder: PropTypes.string +}; + +export default injectIntl(ImportModal); diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css index 858beb5bad1de1eeaf30e9cdbc786621cfa3723a..c6e5037e8c60cd226103cdcc3f4131ebe8aa9745 100644 --- a/src/components/preview-modal/preview-modal.css +++ b/src/components/preview-modal/preview-modal.css @@ -51,7 +51,7 @@ .button-row button { border: 1px solid $motion-primary; border-radius: 0.25rem; - padding: 0.5rem 2rem; + padding: 0.5rem 1.5rem; background: white; font-weight: bold; font-size: .875rem; @@ -66,6 +66,13 @@ .button-row button.no-button { color: $motion-primary; } + +.button-row button.view-project-button { + background: $import-primary; + border-color: $import-primary; + color: white; +} + .button-row button + button { margin-left: 0.5rem; } diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx index 71a3b029ca86bb8793f3aa8848c11b0e91294a9d..5772b518539a7541f30859a3aef6540eef030878 100644 --- a/src/components/preview-modal/preview-modal.jsx +++ b/src/components/preview-modal/preview-modal.jsx @@ -71,6 +71,17 @@ const PreviewModal = ({intl, ...props}) => ( }} /> </button> + <button + className={styles.viewProjectButton} + title="viewproject" + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View 2.0 Project" + description="Label for button to import a 2.0 project" + id="gui.previewModal.viewproject" + /> + </button> </Box> <Box className={styles.faqLinkText}> <FormattedMessage @@ -100,7 +111,8 @@ const PreviewModal = ({intl, ...props}) => ( PreviewModal.propTypes = { intl: intlShape.isRequired, onCancel: PropTypes.func.isRequired, - onTryIt: PropTypes.func.isRequired + onTryIt: PropTypes.func.isRequired, + onViewProject: PropTypes.func.isRequired }; export default injectIntl(PreviewModal); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index bae6f9684ec5e3553a2d1eeb4e32eb10da4793d0..de4fe27a284623c4058f07e62da59d32fd378643 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -53,6 +53,7 @@ class GUI extends React.Component { GUI.propTypes = { ...GUIComponent.propTypes, feedbackFormVisible: PropTypes.bool, + importInfoVisible: PropTypes.bool, previewInfoVisible: PropTypes.bool, projectData: PropTypes.string, vm: PropTypes.instanceOf(VM) @@ -65,6 +66,7 @@ const mapStateToProps = state => ({ blocksTabVisible: state.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, costumesTabVisible: state.editorTab.activeTabIndex === COSTUMES_TAB_INDEX, feedbackFormVisible: state.modals.feedbackForm, + importInfoVisible: state.modals.importInfo, previewInfoVisible: state.modals.previewInfo, soundsTabVisible: state.editorTab.activeTabIndex === SOUNDS_TAB_INDEX }); diff --git a/src/containers/import-modal.jsx b/src/containers/import-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1c2ddbefbb4639cec62cbf37fc7ceae33a45dbc8 --- /dev/null +++ b/src/containers/import-modal.jsx @@ -0,0 +1,111 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; + +import ImportModalComponent from '../components/import-modal/import-modal.jsx'; + +import { + closeImportInfo, + openPreviewInfo +} from '../reducers/modals'; + +class ImportModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleViewProject', + 'handleCancel', + 'handleChange', + 'handleGoBack', + 'handleKeyPress' + ]); + + this.state = { + inputValue: '', + hasValidationError: false, + errorMessage: '' + }; + } + handleKeyPress (event) { + if (event.key === 'Enter') this.handleViewProject(); + } + handleViewProject () { + const inputValue = this.state.inputValue; + const projectId = this.validate(inputValue); + + if (projectId) { + const projectLink = document.createElement('a'); + document.body.appendChild(projectLink); + projectLink.href = `#${projectId}`; + projectLink.click(); + document.body.removeChild(projectLink); + this.props.onViewProject(); + } else { + this.setState({ + hasValidationError: true, + errorMessage: `invalidFormatError`}); + } + } + handleChange (e) { + this.setState({inputValue: e.target.value, hasValidationError: false}); + } + validate (input) { + const urlMatches = input.match(/^(?:https:\/\/)?scratch\.mit\.edu\/projects\/(\d+)/); + if (urlMatches && urlMatches.length > 0) { + return urlMatches[1]; + } + const projectIdMatches = input.match(/^#?(\d+)$/); + if (projectIdMatches && projectIdMatches.length > 0) { + return projectIdMatches[1]; + } + return null; + } + handleCancel () { + this.props.onCancel(); + } + handleGoBack () { + this.props.onBack(); + } + render () { + return ( + <ImportModalComponent + errorMessage={this.state.errorMessage} + hasValidationError={this.state.hasValidationError} + inputValue={this.state.inputValue} + placeholder="scratch.mit.edu/projects/123456789" + onCancel={this.handleCancel} + onChange={this.handleChange} + onGoBack={this.handleGoBack} + onKeyPress={this.handleKeyPress} + onViewProject={this.handleViewProject} + /> + ); + } +} + +ImportModal.propTypes = { + onBack: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onViewProject: PropTypes.func +}; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => ({ + onBack: () => { + dispatch(closeImportInfo()); + dispatch(openPreviewInfo()); + }, + onCancel: () => { + dispatch(closeImportInfo()); + }, + onViewProject: () => { + dispatch(closeImportInfo()); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ImportModal); diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx index 8d50dc83c279e5c4b1660b012b7f15cd83344c3b..1209f251f0445da6a5aa0d65f8fa6d343ce5654e 100644 --- a/src/containers/preview-modal.jsx +++ b/src/containers/preview-modal.jsx @@ -8,7 +8,8 @@ import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; import { - closePreviewInfo + closePreviewInfo, + openImportInfo } from '../reducers/modals'; class PreviewModal extends React.Component { @@ -16,7 +17,8 @@ class PreviewModal extends React.Component { super(props); bindAll(this, [ 'handleTryIt', - 'handleCancel' + 'handleCancel', + 'handleViewProject' ]); this.state = { @@ -30,6 +32,9 @@ class PreviewModal extends React.Component { handleCancel () { window.location.replace('https://scratch.mit.edu'); } + handleViewProject () { + this.props.onViewProject(); + } supportedBrowser () { if (platform.name === 'IE') { return false; @@ -42,6 +47,7 @@ class PreviewModal extends React.Component { previewing={this.state.previewing} onCancel={this.handleCancel} onTryIt={this.handleTryIt} + onViewProject={this.handleViewProject} /> : <BrowserModalComponent onBack={this.handleCancel} @@ -51,7 +57,8 @@ class PreviewModal extends React.Component { } PreviewModal.propTypes = { - onTryIt: PropTypes.func + onTryIt: PropTypes.func, + onViewProject: PropTypes.func }; const mapStateToProps = () => ({}); @@ -59,6 +66,10 @@ const mapStateToProps = () => ({}); const mapDispatchToProps = dispatch => ({ onTryIt: () => { dispatch(closePreviewInfo()); + }, + onViewProject: () => { + dispatch(closePreviewInfo()); + dispatch(openImportInfo()); } }); diff --git a/src/css/colors.css b/src/css/colors.css index f4611c06e25e2ffad536fd7520186dd57fd29101..be8a9a0dbe1ef45a5a3214f1d8d00b92f4b74d42 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -21,3 +21,5 @@ $data-primary: #FF8C1A; $pen-primary: #11B581; $form-border: #E9EEF2; + +$import-primary: #0FBD8C; diff --git a/src/reducers/modals.js b/src/reducers/modals.js index ffbca3f8c07feb4326c83e2de284eb2bf4108fc5..ba038ff3464e2d3c33373e5fb54f9d92fe1544f3 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -7,6 +7,7 @@ const MODAL_BACKDROP_LIBRARY = 'backdropLibrary'; const MODAL_COSTUME_LIBRARY = 'costumeLibrary'; const MODAL_EXTENSION_LIBRARY = 'extensionLibrary'; const MODAL_FEEDBACK_FORM = 'feedbackForm'; +const MODAL_IMPORT_INFO = 'importInfo'; const MODAL_PREVIEW_INFO = 'previewInfo'; const MODAL_SOUND_LIBRARY = 'soundLibrary'; const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; @@ -18,6 +19,7 @@ const initialState = { [MODAL_COSTUME_LIBRARY]: false, [MODAL_EXTENSION_LIBRARY]: false, [MODAL_FEEDBACK_FORM]: false, + [MODAL_IMPORT_INFO]: false, [MODAL_PREVIEW_INFO]: true, [MODAL_SOUND_LIBRARY]: false, [MODAL_SPRITE_LIBRARY]: false, @@ -67,6 +69,14 @@ const openFeedbackForm = function () { analytics.pageview('/modals/feedback'); return openModal(MODAL_FEEDBACK_FORM); }; +const openImportInfo = function () { + analytics.pageview('modals/import'); + return openModal(MODAL_IMPORT_INFO); +}; +const openPreviewInfo = function () { + analytics.pageview('/modals/preview'); + return openModal(MODAL_PREVIEW_INFO); +}; const openSoundLibrary = function () { analytics.pageview('/libraries/sounds'); return openModal(MODAL_SOUND_LIBRARY); @@ -79,10 +89,6 @@ const openSoundRecorder = function () { analytics.pageview('/modals/microphone'); return openModal(MODAL_SOUND_RECORDER); }; -const openPreviewInfo = function () { - analytics.pageview('/modals/preview'); - return openModal(MODAL_PREVIEW_INFO); -}; const closeBackdropLibrary = function () { return closeModal(MODAL_BACKDROP_LIBRARY); }; @@ -95,6 +101,9 @@ const closeExtensionLibrary = function () { const closeFeedbackForm = function () { return closeModal(MODAL_FEEDBACK_FORM); }; +const closeImportInfo = function () { + return closeModal(MODAL_IMPORT_INFO); +}; const closePreviewInfo = function () { return closeModal(MODAL_PREVIEW_INFO); }; @@ -113,6 +122,7 @@ export { openCostumeLibrary, openExtensionLibrary, openFeedbackForm, + openImportInfo, openPreviewInfo, openSoundLibrary, openSpriteLibrary, @@ -121,6 +131,7 @@ export { closeCostumeLibrary, closeExtensionLibrary, closeFeedbackForm, + closeImportInfo, closePreviewInfo, closeSpriteLibrary, closeSoundLibrary, diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js index 9d5cb0384252aa9aacb96bd3a780173050dda366..6add67c42b178055b653bf94cf9a29c41a5db9cc 100644 --- a/test/integration/project-loading.test.js +++ b/test/integration/project-loading.test.js @@ -4,6 +4,7 @@ import SeleniumHelper from '../helpers/selenium-helper'; const { clickText, clickXpath, + findByXpath, getDriver, getLogs, loadUri @@ -31,7 +32,40 @@ describe('Loading scratch gui', () => { describe('Loading projects by ID', () => { - test('Load a project by ID', async () => { + test('Load 2.0 project using import modal', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + const el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + const projectId = '96708228'; + await el.sendKeys(`scratch.mit.edu/projects/${projectId}`); + await clickXpath('//button[@title="viewproject"]'); + 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(); + await expect(logs).toEqual([]); + }); + + test('Invalid url when loading project through modal lets you try again', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + let el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + await el.sendKeys('thisisnotaurl'); + await clickXpath('//button[@title="viewproject"]'); + el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + await el.clear(); + await el.sendKeys('scratch.mit.edu/projects/96708228'); + await clickXpath('//button[@title="viewproject"]'); + 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(); + await expect(logs).toEqual([]); + }); + + test('Load a project by ID directly through url', async () => { const projectId = '96708228'; await loadUri(`${uri}#${projectId}`); await clickXpath('//button[@title="tryit"]');