diff --git a/README.md b/README.md index e114acb5c80cb62555d3371c56b59562ab1231ee..91613cdcdc36a486a997cb8bf97889457f64d73e 100644 --- a/README.md +++ b/README.md @@ -47,32 +47,69 @@ Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm * Follow the recipe above step by step and don't change the order. It is especially important to run npm first because installing after the linking will reset the linking. * Make sure the repositories are siblings on your machine's file tree. * If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them. -* In the worst case unlink the repositories with `npm unlink` and start over. +* In the worst case unlink the repositories by running `npm unlink` in both, and start over. ## 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, build, and integration tests. +### Documentation + +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. + +See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. + +### Running tests + +*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.* + +Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level. + +#### Main testing command + +To run linter, unit tests, build, and integration tests, all at once: ```bash npm test ``` -Run unit tests in isolation. +#### Running unit tests + +To run unit tests in isolation: +```bash +npm run test:unit +``` + +To run unit tests in watch mode (watches for code changes and continuously runs tests): ```bash -npm run unit-test +npm run test:unit -- --watch ``` -Run unit tests in watch mode (watches for code changes and continuously runs tests). See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. +#### Running integration tests + +Integration tests use a headless browser to manipulate the actual html and javascript that the repo +produces. You will not see this activity (though you can hear it when sounds are played!). + +Note that integration tests require you to first create a build that can be loaded in a browser: + ```bash -npm run unit-test -- --watch +npm run build ``` -Run integration tests in isolation. +Then, you can run all integration tests: + ```bash -npm run integration-test +npm run test:integration ``` -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. +Or, you can run a single file of integration tests (in this example, the `backpack` tests): + +```bash +$(npm bin)/jest --runInBand test/integration/backpack.test.js +``` + +If you want to watch the browser as it runs the test, rather than running headless, use: + +```bash +USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js +``` ## Publishing to GitHub Pages You can publish the GUI to github.io so that others on the Internet can view it. diff --git a/package.json b/package.json index e622d0e06b53fe506be4b077842cdef1b96617d3..27d80a6410f9beaea97855459de09ef70b8fcea4 100644 --- a/package.json +++ b/package.json @@ -98,10 +98,10 @@ "scratch-audio": "0.1.0-prerelease.20180625202813", "scratch-blocks": "0.1.0-prerelease.1535662135", "scratch-l10n": "3.0.20180830210150", - "scratch-paint": "0.2.0-prerelease.20180905220723", - "scratch-render": "0.1.0-prerelease.20180824141819", + "scratch-paint": "0.2.0-prerelease.20180907144642", + "scratch-render": "0.1.0-prerelease.20180907144714", "scratch-storage": "1.0.2", - "scratch-svg-renderer": "0.2.0-prerelease.20180817005452", + "scratch-svg-renderer": "0.2.0-prerelease.20180907141232", "scratch-vm": "0.2.0-prerelease.20180907210751", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 71c623135093df1f503ff64c93a57183eb33e761..aa1dfc0393388111d80c4179e21b7a0d62fa95f1 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -74,6 +74,7 @@ const GUIComponent = props => { onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onSeeCommunity, + onUpdateProjectTitle, previewInfoVisible, targetIsStage, soundsTabVisible, @@ -147,6 +148,7 @@ const GUIComponent = props => { <MenuBar enableCommunity={enableCommunity} onSeeCommunity={onSeeCommunity} + onUpdateProjectTitle={onUpdateProjectTitle} /> <Box className={styles.bodyWrapper}> <Box className={styles.flexWrapper}> @@ -294,6 +296,7 @@ GUIComponent.propTypes = { onRequestCloseCostumeLibrary: PropTypes.func, onSeeCommunity: PropTypes.func, onTabSelect: PropTypes.func, + onUpdateProjectTitle: PropTypes.func, previewInfoVisible: PropTypes.bool, soundsTabVisible: PropTypes.bool, stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 662c0e1ae6fe2e403367d2d68194b81b83a0f409..f55fccb153abd7f9af4540cbc3999c84324541fb 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -109,22 +109,6 @@ height: 34px; } -.title-field { - border: 1px dashed $ui-black-transparent; - border-radius: .25rem; - width: 12rem; - height: 34px; - background-color: transparent; - padding: .5rem; -} - -.title-field, -.title-field::placeholder { - color: $ui-white; - font-weight: bold; - font-size: .8rem; -} - .share-button { background: $data-primary; height: 32px; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index b1ccd29412e736cd5c39b6297e17ddaf3e190668..82b37b0b058c18380f74a5b017daada9e61a74b1 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -13,6 +13,7 @@ import LanguageSelector from '../../containers/language-selector.jsx'; import ProjectLoader from '../../containers/project-loader.jsx'; import Menu from '../../containers/menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import ProjectTitleInput from './project-title-input.jsx'; import ProjectSaver from '../../containers/project-saver.jsx'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; @@ -393,11 +394,12 @@ class MenuBar extends React.Component { </div> <Divider className={classNames(styles.divider)} /> <div className={classNames(styles.menuBarItem)}> - <MenuBarItemTooltip id="title-field"> - <input - disabled - className={classNames(styles.titleField)} - placeholder="Untitled-1" + <MenuBarItemTooltip + enable + id="title-field" + > + <ProjectTitleInput + onUpdateProjectTitle={this.props.onUpdateProjectTitle} /> </MenuBarItemTooltip> </div> @@ -521,7 +523,8 @@ MenuBar.propTypes = { onRequestCloseEdit: PropTypes.func, onRequestCloseFile: PropTypes.func, onRequestCloseLanguage: PropTypes.func, - onSeeCommunity: PropTypes.func + onSeeCommunity: PropTypes.func, + onUpdateProjectTitle: PropTypes.func }; const mapStateToProps = state => ({ diff --git a/src/components/menu-bar/project-title-input.css b/src/components/menu-bar/project-title-input.css new file mode 100644 index 0000000000000000000000000000000000000000..84dcd0afcda19a459d091d4877ab52ea23bdf19c --- /dev/null +++ b/src/components/menu-bar/project-title-input.css @@ -0,0 +1,44 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/z-index.css"; + +$title-width: 12rem; + +.title-field { + border: 1px dashed $ui-black-transparent; + border-radius: $form-radius; + -webkit-border-radius: $form-radius; + -moz-border-radius: $form-radius; + background-color: $ui-white-transparent; + background-clip: padding-box; + -webkit-background-clip: padding-box; + width: $title-width; + height: auto; + padding: .5rem; +} + +.title-field { + color: $ui-white; + font-weight: bold; + font-size: .8rem; +} + +.title-field::placeholder { + color: $ui-white; + font-weight: normal; + font-size: .8rem; + font-style: italic; +} + +.title-field:hover { + background-color: hsla(0, 100%, 100%, 0.5); +} + +.title-field:focus { + outline:none; + border: 1px solid $ui-transparent; + -webkit-box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent; + box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent; + background-color: $ui-white; + color: $text-primary; +} diff --git a/src/components/menu-bar/project-title-input.jsx b/src/components/menu-bar/project-title-input.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8b927417fddb717b4ff2fdfdd948e0bf7222e873 --- /dev/null +++ b/src/components/menu-bar/project-title-input.jsx @@ -0,0 +1,66 @@ +import classNames from 'classnames'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; +import React from 'react'; +import {defineMessages, intlShape, injectIntl} from 'react-intl'; + +import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import Input from '../forms/input.jsx'; +const BufferedInput = BufferedInputHOC(Input); + +import styles from './project-title-input.css'; + +const messages = defineMessages({ + projectTitlePlaceholder: { + id: 'gui.gui.projectTitlePlaceholder', + description: 'Placeholder for project title when blank', + defaultMessage: 'Project title here' + } +}); + +class ProjectTitleInput extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleUpdateProjectTitle' + ]); + } + // call onUpdateProjectTitle if it is defined (only defined when gui + // is used within scratch-www) + handleUpdateProjectTitle (newTitle) { + if (this.props.onUpdateProjectTitle) { + this.props.onUpdateProjectTitle(newTitle); + } + } + render () { + return ( + <BufferedInput + className={classNames(styles.titleField)} + maxLength="100" + placeholder={this.props.intl.formatMessage(messages.projectTitlePlaceholder)} + tabIndex="0" + type="text" + value={this.props.projectTitle} + onSubmit={this.handleUpdateProjectTitle} + /> + ); + } +} + +ProjectTitleInput.propTypes = { + intl: intlShape.isRequired, + onUpdateProjectTitle: PropTypes.func, + projectTitle: PropTypes.string +}; + +const mapStateToProps = state => ({ + projectTitle: state.scratchGui.projectTitle +}); + +const mapDispatchToProps = () => ({}); + +export default injectIntl(connect( + mapStateToProps, + mapDispatchToProps +)(ProjectTitleInput)); diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index 42de1599939801b8896374f032525be7c77982a9..78192f3adaac829916243929db569003bab283c0 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -48,6 +48,7 @@ const PromptComponent = props => ( <input autoFocus className={styles.variableNameTextInput} + name={props.label} placeholder={props.placeholder} onChange={props.onChange} onKeyPress={props.onKeyPress} diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index f08e6bab746dd171f040e48e916663bbf438538b..04c6142dce25e073c4f0a0676cd2ab3e5c37590d 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -7,6 +7,7 @@ import ReactModal from 'react-modal'; import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import {openExtensionLibrary} from '../reducers/modals'; +import {setProjectTitle} from '../reducers/project-title'; import { activateTab, BLOCKS_TAB_INDEX, @@ -34,6 +35,10 @@ class GUI extends React.Component { }; } componentDidMount () { + if (this.props.projectTitle) { + this.props.onUpdateReduxProjectTitle(this.props.projectTitle); + } + if (this.props.vm.initialized) return; this.audioEngine = new AudioEngine(); this.props.vm.attachAudioEngine(this.audioEngine); @@ -65,6 +70,9 @@ class GUI extends React.Component { }); }); } + if (this.props.projectTitle !== nextProps.projectTitle) { + this.props.onUpdateReduxProjectTitle(nextProps.projectTitle); + } } render () { if (this.state.loadingError) { @@ -75,8 +83,10 @@ class GUI extends React.Component { /* eslint-disable no-unused-vars */ assetHost, hideIntro, + onUpdateReduxProjectTitle, projectData, projectHost, + projectTitle, /* eslint-enable no-unused-vars */ children, fetchingProject, @@ -97,14 +107,19 @@ class GUI extends React.Component { } GUI.propTypes = { + assetHost: PropTypes.string, children: PropTypes.node, fetchingProject: PropTypes.bool, hideIntro: PropTypes.bool, importInfoVisible: PropTypes.bool, loadingStateVisible: PropTypes.bool, onSeeCommunity: PropTypes.func, + onUpdateProjectTitle: PropTypes.func, + onUpdateReduxProjectTitle: PropTypes.func, previewInfoVisible: PropTypes.bool, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + projectHost: PropTypes.string, + projectTitle: PropTypes.string, vm: PropTypes.instanceOf(VM) }; @@ -134,7 +149,8 @@ const mapDispatchToProps = dispatch => ({ onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)), onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)), onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()), - onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()) + onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()), + onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title)) }); const ConnectedGUI = connect( diff --git a/src/containers/project-loader.jsx b/src/containers/project-loader.jsx index 507b716523796e14fb9b73fbb93cca44df2785f8..66081b7a906871cef4a5230d9264cc434c2d3dcd 100644 --- a/src/containers/project-loader.jsx +++ b/src/containers/project-loader.jsx @@ -6,6 +6,7 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import analytics from '../lib/analytics'; import log from '../lib/log'; +import {setProjectTitle} from '../reducers/project-title'; import { openLoadingProject, @@ -75,6 +76,12 @@ class ProjectLoader extends React.Component { if (thisFileInput.files) { // Don't attempt to load if no file was selected this.props.openLoadingState(); reader.readAsArrayBuffer(thisFileInput.files[0]); + if (thisFileInput.files[0].name) { + const matches = thisFileInput.files[0].name.match(/^(.*)\.sb3$/); + if (matches) { + this.props.onSetProjectTitle(matches[1].substring(0, 100)); + } + } } } handleClick () { @@ -112,6 +119,7 @@ ProjectLoader.propTypes = { children: PropTypes.func, closeLoadingState: PropTypes.func, intl: intlShape.isRequired, + onSetProjectTitle: PropTypes.func, openLoadingState: PropTypes.func, vm: PropTypes.shape({ loadProject: PropTypes.func @@ -124,6 +132,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ closeLoadingState: () => dispatch(closeLoadingProject()), + onSetProjectTitle: title => dispatch(setProjectTitle(title)), openLoadingState: () => dispatch(openLoadingProject()) }); diff --git a/src/containers/project-saver.jsx b/src/containers/project-saver.jsx index 13be33da808b1fa2cf9e10c6d2db2644112ef2e0..c32d4b1bf0a045b6c20f9d9593cf5548aebf922b 100644 --- a/src/containers/project-saver.jsx +++ b/src/containers/project-saver.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import storage from '../lib/storage'; +import {projectTitleInitialState} from '../reducers/project-title'; + /** * Project saver component passes a saveProject function to its child. @@ -33,21 +35,15 @@ class ProjectSaver extends React.Component { document.body.appendChild(saveLink); this.props.saveProjectSb3().then(content => { - // TODO user-friendly project name - // File name: project-DATE-TIME - const date = new Date(); - const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`; - const filename = `untitled-project-${timestamp}.sb3`; - // Use special ms version if available to get it working on Edge. if (navigator.msSaveOrOpenBlob) { - navigator.msSaveOrOpenBlob(content, filename); + navigator.msSaveOrOpenBlob(content, this.props.projectFilename); return; } const url = window.URL.createObjectURL(content); saveLink.href = url; - saveLink.download = filename; + saveLink.download = this.props.projectFilename; saveLink.click(); window.URL.revokeObjectURL(url); document.body.removeChild(saveLink); @@ -86,14 +82,24 @@ class ProjectSaver extends React.Component { } } +const getProjectFilename = (curTitle, defaultTitle) => { + let filenameTitle = curTitle; + if (!filenameTitle || filenameTitle.length === 0) { + filenameTitle = defaultTitle; + } + return `${filenameTitle.substring(0, 100)}.sb3`; +}; + ProjectSaver.propTypes = { children: PropTypes.func, + projectFilename: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), saveProjectSb3: PropTypes.func }; const mapStateToProps = state => ({ saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm), + projectFilename: getProjectFilename(state.scratchGui.projectTitle, projectTitleInitialState), projectId: state.scratchGui.projectId }); diff --git a/src/css/colors.css b/src/css/colors.css index 37ffa62227a6f6ccb42a6170d4652db037f4f769..2222608922a9df36770fb8330fb663b2ea1e43c7 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -6,6 +6,7 @@ $ui-modal-overlay: hsla(215, 100%, 65%, 0.9); /* 90% transparent version of moti $ui-white: hsla(0, 100%, 100%, 1); /* #FFFFFF */ $ui-white-transparent: hsla(0, 100%, 100%, 0.25); /* 25% transparent version of ui-white */ +$ui-transparent: hsla(0, 100%, 100%, 0); /* 25% transparent version of ui-white */ $ui-black-transparent: hsla(0, 0%, 0%, 0.15); /* 15% transparent version of black */ diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index eafd49cb32606437837a1e3837ae1812d9507f51..571f220fdea8786a24f8d557ae6c72fde38b37de 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -72,7 +72,6 @@ const ProjectLoaderHOC = function (WrappedComponent) { assetHost, projectHost, projectId, - reduxProjectId, setProjectId: setProjectIdProp, /* eslint-enable no-unused-vars */ ...componentProps diff --git a/src/lib/titled-hoc.jsx b/src/lib/titled-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2534d96bd61ba9f4cb4db89a51e33c031f1be6c4 --- /dev/null +++ b/src/lib/titled-hoc.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import bindAll from 'lodash.bindall'; +import {defineMessages, intlShape, injectIntl} from 'react-intl'; + +const messages = defineMessages({ + defaultProjectTitle: { + id: 'gui.gui.defaultProjectTitle', + description: 'Default title for project', + defaultMessage: 'Scratch Project' + } +}); + +/* Higher Order Component to get and set the project title + * @param {React.Component} WrappedComponent component to receive project title related props + * @returns {React.Component} component with project loading behavior + */ +const TitledHOC = function (WrappedComponent) { + class TitledComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleUpdateProjectTitle' + ]); + this.state = { + projectTitle: this.props.intl.formatMessage(messages.defaultProjectTitle) + }; + } + handleUpdateProjectTitle (newTitle) { + this.setState({projectTitle: newTitle}); + } + render () { + return ( + <WrappedComponent + projectTitle={this.state.projectTitle} + onUpdateProjectTitle={this.handleUpdateProjectTitle} + {...this.props} + /> + ); + } + } + + TitledComponent.propTypes = { + intl: intlShape.isRequired + }; + + // return TitledComponent; + const IntlTitledComponent = injectIntl(TitledComponent); + return IntlTitledComponent; + +}; + +export { + TitledHOC as default +}; diff --git a/src/playground/player.jsx b/src/playground/player.jsx index 6c2833f1b8108d08022e9d6c85bac9357f1f497f..e236de667eb4b12919548908daad377dfa827fe9 100644 --- a/src/playground/player.jsx +++ b/src/playground/player.jsx @@ -8,6 +8,7 @@ import Box from '../components/box/box.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; import AppStateHOC from '../lib/app-state-hoc.jsx'; +import TitledHOC from '../lib/titled-hoc.jsx'; import {setPlayer} from '../reducers/mode'; @@ -48,7 +49,7 @@ const mapDispatchToProps = dispatch => ({ }); const ConnectedPlayer = connect(mapStateToProps, mapDispatchToProps)(Player); -const WrappedPlayer = HashParserHOC(AppStateHOC(ConnectedPlayer)); +const WrappedPlayer = HashParserHOC(AppStateHOC(TitledHOC(ConnectedPlayer))); const appTarget = document.createElement('div'); document.body.appendChild(appTarget); diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index adec922b8c206c9211888da34a8c426362b9a10b..562e57eac9c4c60ad4aea322adeec8b907067761 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom'; import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; +import TitledHOC from '../lib/titled-hoc.jsx'; /* * Render the GUI playground. This is a separate function because importing anything @@ -12,7 +13,7 @@ import HashParserHOC from '../lib/hash-parser-hoc.jsx'; */ export default appTarget => { GUI.setAppElement(appTarget); - const WrappedGui = HashParserHOC(AppStateHOC(GUI)); + const WrappedGui = HashParserHOC(AppStateHOC(TitledHOC(GUI))); // TODO a hack for testing the backpack, allow backpack host to be set by url param const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 9340af56e1177485aa02c361c404070b6b4c6bf3..cfad8f6825ddad3a07a56b4c62757ffbc3a97909 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -12,6 +12,7 @@ import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; import projectIdReducer, {projectIdInitialState} from './project-id'; +import projectTitleReducer, {projectTitleInitialState} from './project-title'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; @@ -37,6 +38,7 @@ const guiInitialState = { monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, projectId: projectIdInitialState, + projectTitle: projectTitleInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, toolbox: toolboxInitialState, @@ -80,6 +82,7 @@ const guiReducer = combineReducers({ monitors: monitorReducer, monitorLayout: monitorLayoutReducer, projectId: projectIdReducer, + projectTitle: projectTitleReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, toolbox: toolboxReducer, diff --git a/src/reducers/project-title.js b/src/reducers/project-title.js new file mode 100644 index 0000000000000000000000000000000000000000..09bf6c4eab417e626b8a719326ac685c25d46cdd --- /dev/null +++ b/src/reducers/project-title.js @@ -0,0 +1,25 @@ +const SET_PROJECT_TITLE = 'projectTitle/SET_PROJECT_TITLE'; + +// we are initializing to a blank string instead of an actual title, +// because it would be hard to localize here +const initialState = ''; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_PROJECT_TITLE: + return action.title; + default: + return state; + } +}; +const setProjectTitle = title => ({ + type: SET_PROJECT_TITLE, + title: title +}); + +export { + reducer as default, + initialState as projectTitleInitialState, + setProjectTitle +}; diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js index 83b72620bcc04dad457f157071a54016f74381aa..b3ec62e2f6042c69ccb5f7ea985d8267e73da93e 100644 --- a/test/integration/blocks.test.js +++ b/test/integration/blocks.test.js @@ -68,11 +68,11 @@ describe('Working with the blocks', () => { await findByText('0', scope.reportedValue); await clickText('Make a Variable'); - let el = await findByXpath("//input[@placeholder='']"); + let el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('score'); await clickButton('OK'); await clickText('Make a Variable'); - el = await findByXpath("//input[@placeholder='']"); + el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('second variable'); await clickButton('OK'); @@ -98,7 +98,7 @@ describe('Working with the blocks', () => { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Make a List'); - let el = await findByXpath("//input[@placeholder='']"); + let el = await findByXpath("//input[@name='New list name:']"); await el.sendKeys('list1'); await clickButton('OK'); diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js index ecaeb56f2fa36f9bd83095563395f72a7759426f..8408c88f39bc949ef360095c1997252acf35428f 100644 --- a/test/integration/examples.test.js +++ b/test/integration/examples.test.js @@ -87,11 +87,11 @@ describe('blocks example', () => { await clickText('Variables'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Make a Variable'); - let el = await findByXpath("//input[@placeholder='']"); + let el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('score'); await clickButton('OK'); await clickText('Make a Variable'); - el = await findByXpath("//input[@placeholder='']"); + el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('second variable'); await clickButton('OK'); const logs = await getLogs();