
@@ -116,6 +118,7 @@ class SpriteInfo extends React.Component {
)}
tabIndex="4"
onClick={this.props.onClickVisible}
+ onKeyPress={this.props.onPressVisible}
>
![]()
blockInfo.json));
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
- const toolboxXML = makeToolboxXML(dynamicBlocksXML);
- this.props.onExtensionAdded(toolboxXML);
+ const toolboxXML = makeToolboxXML(this.props.vm.editingTarget.id, dynamicBlocksXML);
+ this.props.updateToolboxState(toolboxXML);
}
handleCategorySelected (categoryName) {
this.workspace.toolbox_.setSelectedCategoryByName(categoryName);
@@ -220,7 +227,7 @@ class Blocks extends React.Component {
vm,
isVisible,
onActivateColorPicker,
- onExtensionAdded,
+ updateToolboxState,
onRequestCloseExtensionLibrary,
toolboxXML,
...props
@@ -257,7 +264,6 @@ Blocks.propTypes = {
extensionLibraryVisible: PropTypes.bool,
isVisible: PropTypes.bool,
onActivateColorPicker: PropTypes.func,
- onExtensionAdded: PropTypes.func,
onRequestCloseExtensionLibrary: PropTypes.func,
options: PropTypes.shape({
media: PropTypes.string,
@@ -281,6 +287,7 @@ Blocks.propTypes = {
comments: PropTypes.bool
}),
toolboxXML: PropTypes.string,
+ updateToolboxState: PropTypes.func,
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -322,11 +329,11 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
- onExtensionAdded: toolboxXML => {
- dispatch(updateToolbox(toolboxXML));
- },
onRequestCloseExtensionLibrary: () => {
dispatch(closeExtensionLibrary());
+ },
+ updateToolboxState: toolboxXML => {
+ dispatch(updateToolbox(toolboxXML));
}
});
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 1984923d5..6143175dd 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -52,20 +52,18 @@ class SoundTab extends React.Component {
render () {
const {
- editingTarget,
- sprites,
- stage,
+ vm,
onNewSoundFromLibraryClick,
onNewSoundFromRecordingClick
} = this.props;
- const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
-
- if (!target) {
+ if (!vm.editingTarget) {
return null;
}
- const sounds = target.sounds ? target.sounds.map(sound => (
+ const sprite = vm.editingTarget.sprite;
+
+ const sounds = sprite.sounds ? sprite.sounds.map(sound => (
{
url: soundIcon,
name: sound.name
@@ -106,7 +104,7 @@ class SoundTab extends React.Component {
onDeleteClick={this.handleDeleteSound}
onItemClick={this.handleSelectSound}
>
- {editingTarget && target.sounds && target.sounds.length > 0 ? (
+ {sprite.sounds && sprite.sounds.length > 0 ? (
) : null}
{this.props.soundRecorderVisible ? (
diff --git a/src/containers/sprite-info.jsx b/src/containers/sprite-info.jsx
index 532fb37e5..f4b5ca696 100644
--- a/src/containers/sprite-info.jsx
+++ b/src/containers/sprite-info.jsx
@@ -10,7 +10,9 @@ class SpriteInfo extends React.Component {
bindAll(this, [
'handleChangeRotationStyle',
'handleClickVisible',
- 'handleClickNotVisible'
+ 'handleClickNotVisible',
+ 'handlePressVisible',
+ 'handlePressNotVisible'
]);
}
handleChangeRotationStyle (e) {
@@ -24,6 +26,18 @@ class SpriteInfo extends React.Component {
e.preventDefault();
this.props.onChangeVisibility(false);
}
+ handlePressVisible (e) {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ this.props.onChangeVisibility(true);
+ }
+ }
+ handlePressNotVisible (e) {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ this.props.onChangeVisibility(false);
+ }
+ }
render () {
return (
);
}
diff --git a/src/css/units.css b/src/css/units.css
index 3de6174a2..3e7f73f36 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -9,3 +9,7 @@ $stage-menu-height: 2.75rem;
$library-header-height: 4.375rem;
$form-radius: calc($space / 2);
+
+/* layout contants from `layout-constants.js` */
+$full-size: 1095px;
+$full-size-paint: 1249px;
diff --git a/src/index.jsx b/src/index.jsx
index 6d301f516..e55fc0ef4 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -7,6 +7,11 @@ import ProjectLoaderHOC from './lib/project-loader-hoc.jsx';
import styles from './index.css';
+if (process.env.NODE_ENV === 'production' && typeof window === 'object') {
+ // Warn before navigating away
+ window.onbeforeunload = () => true;
+}
+
const App = AppStateHOC(ProjectLoaderHOC(GUI));
const appTarget = document.createElement('div');
diff --git a/src/lib/layout-constants.js b/src/lib/layout-constants.js
index 672c8013f..67c2ae44c 100644
--- a/src/lib/layout-constants.js
+++ b/src/lib/layout-constants.js
@@ -3,5 +3,6 @@ export default {
fullStageHeight: 360,
smallerStageWidth: 480 * 0.85,
smallerStageHeight: 360 * 0.85,
- fullSizeMinWidth: 1096
+ fullSizeMinWidth: 1096,
+ fullSizePaintMinWidth: 1250
};
diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 315e1388c..608cf92b3 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -2,7 +2,8 @@ const categorySeparator = '
';
const blockSeparator = '
'; // At default scale, about 28px
-const motion = `
+const motion = function (targetId) {
+ return `
@@ -120,14 +121,16 @@ const motion = `
${blockSeparator}
${blockSeparator}
-
-
-
+
+
+
${categorySeparator}
-`;
+ `;
+};
-const looks = `
+const looks = function (targetId) {
+ return `
@@ -229,15 +232,17 @@ const looks = `
${blockSeparator}
-
+
-
+
${categorySeparator}
-`;
+ `;
+};
-const sound = `
+const sound = function () {
+ return `
@@ -284,9 +289,11 @@ const sound = `
${categorySeparator}
-`;
+ `;
+};
-const events = `
+const events = function () {
+ return `
@@ -317,9 +324,11 @@ const events = `
${categorySeparator}
-`;
+ `;
+};
-const control = `
+const control = function () {
+ return `
@@ -354,9 +363,11 @@ const control = `
${categorySeparator}
-`;
+ `;
+};
-const sensing = `
+const sensing = function () {
+ return `
@@ -422,9 +433,11 @@ const sensing = `
${categorySeparator}
-`;
+ `;
+};
-const operators = `
+const operators = function () {
+ return `
@@ -602,33 +615,37 @@ const operators = `
${categorySeparator}
-`;
+ `;
+};
-const data = `
+const data = function () {
+ return `
-`;
+ `;
+};
const xmlOpen = '
';
const xmlClose = '';
/**
+ * @param {!string} targetId - The current editing target
* @param {string?} categoriesXML - null for default toolbox, or an XML string with
elements.
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
*/
-const makeToolboxXML = function (categoriesXML) {
+const makeToolboxXML = function (targetId, categoriesXML) {
const gap = [categorySeparator];
const everything = [
xmlOpen,
- motion, gap,
- looks, gap,
- sound, gap,
- events, gap,
- control, gap,
- sensing, gap,
- operators, gap,
- data
+ motion(targetId), gap,
+ looks(targetId), gap,
+ sound(targetId), gap,
+ events(targetId), gap,
+ control(targetId), gap,
+ sensing(targetId), gap,
+ operators(targetId), gap,
+ data(targetId)
];
if (categoriesXML) {
diff --git a/src/reducers/toolbox.js b/src/reducers/toolbox.js
index 4b06a807c..3f559884f 100644
--- a/src/reducers/toolbox.js
+++ b/src/reducers/toolbox.js
@@ -1,9 +1,9 @@
const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX';
-
import makeToolboxXML from '../lib/make-toolbox-xml';
const initialState = {
- toolboxXML: makeToolboxXML()
+ // @todo switch this to make the stage's XML
+ toolboxXML: makeToolboxXML('')
};
const reducer = function (state, action) {
diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js
index c4f36ca05..ce3f04fbf 100644
--- a/test/helpers/selenium-helper.js
+++ b/test/helpers/selenium-helper.js
@@ -4,7 +4,7 @@ import bindAll from 'lodash.bindall';
import 'chromedriver'; // register path
import webdriver from 'selenium-webdriver';
-const {By, until} = webdriver;
+const {By, until, Button} = webdriver;
class SeleniumHelper {
constructor () {
@@ -15,7 +15,9 @@ class SeleniumHelper {
'findByText',
'findByXpath',
'getDriver',
- 'getLogs'
+ 'getLogs',
+ 'loadUri',
+ 'rightClickText'
]);
}
@@ -34,6 +36,14 @@ class SeleniumHelper {
return this.findByXpath(`//body//${scope || '*'}//*[contains(text(), '${text}')]`);
}
+ loadUri (uri) {
+ return this.driver
+ .get(`file://${uri}`)
+ .then(() => (
+ this.driver.executeScript('window.onbeforeunload = undefined;')
+ ));
+ }
+
clickXpath (xpath) {
return this.findByXpath(xpath).then(el => el.click());
}
@@ -42,6 +52,12 @@ class SeleniumHelper {
return this.findByText(text, scope).then(el => el.click());
}
+ rightClickText (text, scope) {
+ return this.findByText(text, scope).then(el => this.driver.actions()
+ .click(el, Button.RIGHT)
+ .perform());
+ }
+
clickButton (text) {
return this.clickXpath(`//button[contains(text(), '${text}')]`);
}
diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js
index 7fffa903a..908dbfd36 100644
--- a/test/integration/examples.test.js
+++ b/test/integration/examples.test.js
@@ -9,7 +9,8 @@ const {
clickXpath,
findByXpath,
getDriver,
- getLogs
+ getLogs,
+ loadUri
} = new SeleniumHelper();
const errorWhitelist = [
@@ -31,7 +32,7 @@ describe('player example', () => {
test('Load a project by ID', async () => {
const projectId = '96708228';
- await driver.get(`file://${uri}#${projectId}`);
+ await loadUri(`${uri}#${projectId}`);
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -54,7 +55,7 @@ describe('blocks example', () => {
test('Load a project by ID', async () => {
const projectId = '96708228';
- await driver.get(`file://${uri}#${projectId}`);
+ await loadUri(`${uri}#${projectId}`);
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -64,7 +65,7 @@ describe('blocks example', () => {
});
test('Change categories', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(`${uri}`);
await clickText('Looks');
await clickText('Sound');
await clickText('Events');
diff --git a/test/integration/test.js b/test/integration/test.js
index 86729cf27..cfea4e217 100644
--- a/test/integration/test.js
+++ b/test/integration/test.js
@@ -8,7 +8,9 @@ const {
findByText,
findByXpath,
getDriver,
- getLogs
+ getLogs,
+ loadUri,
+ rightClickText
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
@@ -34,9 +36,8 @@ describe('costumes, sounds and variables', () => {
await driver.quit();
});
-
test('Blocks report when clicked in the toolbox', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(uri);
await clickText('Blocks');
await clickText('Operators', blocksTabScope);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
@@ -47,7 +48,7 @@ describe('costumes, sounds and variables', () => {
});
test('Switching sprites updates the block menus', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(uri);
await clickText('Sound', blocksTabScope);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
// "meow" sound block should be visible
@@ -60,7 +61,7 @@ describe('costumes, sounds and variables', () => {
});
test('Adding a costume', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(uri);
await clickText('Costumes');
await clickText('Add Costume');
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
@@ -73,17 +74,22 @@ describe('costumes, sounds and variables', () => {
});
test('Adding a sound', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(uri);
await clickText('Sounds');
+
+ // Delete the sound
+ await rightClickText('meow', soundsTabScope);
+ await clickText('delete', soundsTabScope);
+ await driver.switchTo().alert()
+ .accept();
+
+ // Add a sound
await clickText('Add Sound');
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('chom');
await clickText('chomp'); // Should close the modal, then click the sounds in the selector
- await clickText('meow', soundsTabScope);
await clickText('chomp', soundsTabScope);
await clickXpath('//button[@title="Play"]');
- await clickText('meow', soundsTabScope);
- await clickXpath('//button[@title="Play"]');
await clickText('Louder');
await clickText('Softer');
@@ -99,7 +105,7 @@ describe('costumes, sounds and variables', () => {
test('Load a project by ID', async () => {
const projectId = '96708228';
- await driver.get(`file://${uri}#${projectId}`);
+ await loadUri(`${uri}#${projectId}`);
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -109,7 +115,7 @@ describe('costumes, sounds and variables', () => {
});
test('Creating variables', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(uri);
await clickText('Blocks');
await clickText('Data', blocksTabScope);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
@@ -133,7 +139,7 @@ describe('costumes, sounds and variables', () => {
});
test('Importing extensions', async () => {
- await driver.get(`file://${uri}`);
+ await loadUri(uri);
await clickText('Blocks');
await clickText('Extensions');
await clickText('Pen', modalScope); // Modal closes
diff --git a/test/unit/components/__snapshots__/icon-button.test.jsx.snap b/test/unit/components/__snapshots__/icon-button.test.jsx.snap
index 3a26f0acf..b5f3e7a10 100644
--- a/test/unit/components/__snapshots__/icon-button.test.jsx.snap
+++ b/test/unit/components/__snapshots__/icon-button.test.jsx.snap
@@ -4,6 +4,7 @@ exports[`IconButtonComponent matches snapshot 1`] = `