Merge branch 'develop' into greenkeeper/react-test-renderer-16.1.1

This commit is contained in:
Paul Kaplan authored and GitHub committed 2017-11-20 15:35:01 -05:00
commit 29fd130dd0
21 files changed
+190 -74

No files matched your search

+5 -5
View File
@@ -42,11 +42,11 @@
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.7",
"enzyme": "^3.1.0",
"enzyme-adapter-react-16": "1.0.4",
"enzyme-adapter-react-16": "1.1.0",
"eslint": "^4.7.1",
"eslint-config-scratch": "^5.0.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-react": "^7.2.1",
"eslint-plugin-react": "^7.5.1",
"file-loader": "1.1.5",
"get-float-time-domain-data": "0.1.0",
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
@@ -67,9 +67,9 @@
"prop-types": "^15.5.10",
"raf": "^3.4.0",
"raw-loader": "0.5.1",
"react": "16.1.0",
"react": "16.1.1",
"react-contextmenu": "2.8.0",
"react-dom": "16.1.0",
"react-dom": "16.1.1",
"react-draggable": "3.0.3",
"react-intl": "2.4.0",
"react-intl-redux": "0.7.0",
@@ -77,7 +77,7 @@
"react-redux": "5.0.6",
"react-responsive": "3.0.0",
"react-style-proptype": "3.1.0",
"react-tabs": "2.1.0",
"react-tabs": "2.1.1",
"react-test-renderer": "16.1.1",
"redux": "3.7.0",
"redux-mock-store": "^1.2.3",
+12
View File
@@ -1,4 +1,5 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.wrapper {
width: 150px;
@@ -51,3 +52,14 @@
min-height: 5rem;
margin: 1rem auto;
}
@media only screen and (max-width: $full-size-paint) {
.wrapper {
width: 80px;
}
.list-item {
width: 4rem;
min-height: 4rem;
}
}
@@ -7,6 +7,7 @@ import closeIcon from './icon--close.svg';
const CloseButton = props => (
<div
aria-label="Close"
className={classNames(
styles.closeButton,
props.className,
@@ -15,6 +16,7 @@ const CloseButton = props => (
[styles.large]: props.size === CloseButton.SIZE_LARGE
}
)}
role="button"
onClick={props.onClick}
>
<img
@@ -11,6 +11,7 @@ const IconButton = ({
}) => (
<div
className={classNames(styles.container, className)}
role="button"
onClick={onClick}
>
<img
@@ -22,3 +22,7 @@
color: #3373cc;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.language-select:focus {
border: 1px solid white;
}
@@ -62,6 +62,11 @@
border-bottom-left-radius: $form-radius;
}
.radio-left:focus {
border-color: #4c97ff;
box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
}
.radio-right {
border-bottom: 1px solid $form-border;
border-top: 1px solid $form-border;
@@ -70,6 +75,11 @@
border-bottom-right-radius: $form-radius;
}
.radio-right:focus {
border-color: #4c97ff;
box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
}
.icon {
width: 100%;
height: 100%;
+8 -2
View File
@@ -54,6 +54,7 @@ class SpriteInfo extends React.Component {
<MediaQuery minWidth={layout.fullSizeMinWidth}>
<div className={styles.iconWrapper}>
<img
aria-hidden="true"
className={classNames(styles.xIcon, styles.icon)}
src={xIcon}
/>
@@ -76,6 +77,7 @@ class SpriteInfo extends React.Component {
<MediaQuery minWidth={layout.fullSizeMinWidth}>
<div className={styles.iconWrapper}>
<img
aria-hidden="true"
className={classNames(styles.yIcon, styles.icon)}
src={yIcon}
/>
@@ -116,6 +118,7 @@ class SpriteInfo extends React.Component {
)}
tabIndex="4"
onClick={this.props.onClickVisible}
onKeyPress={this.props.onPressVisible}
>
<img
className={styles.icon}
@@ -132,8 +135,9 @@ class SpriteInfo extends React.Component {
[styles.isDisabled]: this.props.disabled
}
)}
tabIndex="4"
tabIndex="5"
onClick={this.props.onClickNotVisible}
onKeyPress={this.props.onPressNotVisible}
>
<img
className={styles.icon}
@@ -151,7 +155,7 @@ class SpriteInfo extends React.Component {
small
disabled={this.props.disabled}
label="Direction"
tabIndex="5"
tabIndex="6"
type="text"
value={this.props.disabled ? '' : this.props.direction}
onSubmit={this.props.onChangeDirection}
@@ -200,6 +204,8 @@ SpriteInfo.propTypes = {
onChangeY: PropTypes.func,
onClickNotVisible: PropTypes.func,
onClickVisible: PropTypes.func,
onPressNotVisible: PropTypes.func,
onPressVisible: PropTypes.func,
rotationStyle: PropTypes.oneOf(ROTATION_STYLES),
visible: PropTypes.bool,
x: PropTypes.oneOfType([
+14 -7
View File
@@ -172,6 +172,11 @@ class Blocks extends React.Component {
this.workspace.reportValue(data.id, data.value);
}
onWorkspaceUpdate (data) {
// When we change sprites, update the toolbox to have the new sprite's blocks
if (this.props.vm.editingTarget) {
this.props.updateToolboxState(makeToolboxXML(this.props.vm.editingTarget.id));
}
if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
this.onWorkspaceMetricsChange();
}
@@ -179,6 +184,8 @@ class Blocks extends React.Component {
// Remove and reattach the workspace listener (but allow flyout events)
this.workspace.removeChangeListener(this.props.vm.blockListener);
const dom = this.ScratchBlocks.Xml.textToDom(data.xml);
// @todo This line rerenders toolbox, and the change in the toolbox XML also rerenders the toolbox.
// We should only rerender the toolbox once. See https://github.com/LLK/scratch-gui/issues/901
this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace);
this.workspace.addChangeListener(this.props.vm.blockListener);
@@ -193,8 +200,8 @@ class Blocks extends React.Component {
handleExtensionAdded (blocksInfo) {
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => 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));
}
});
+6 -8
View File
@@ -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 ? (
<SoundEditor soundIndex={this.state.selectedSoundIndex} />
) : null}
{this.props.soundRecorderVisible ? (
+17 -1
View File
@@ -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 (
<SpriteInfoComponent
@@ -31,6 +45,8 @@ class SpriteInfo extends React.Component {
onChangeRotationStyle={this.handleChangeRotationStyle}
onClickNotVisible={this.handleClickNotVisible}
onClickVisible={this.handleClickVisible}
onPressNotVisible={this.handlePressNotVisible}
onPressVisible={this.handlePressVisible}
/>
);
}
+4
View File
@@ -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;
+5
View File
@@ -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');
+2 -1
View File
@@ -3,5 +3,6 @@ export default {
fullStageHeight: 360,
smallerStageWidth: 480 * 0.85,
smallerStageHeight: 360 * 0.85,
fullSizeMinWidth: 1096
fullSizeMinWidth: 1096,
fullSizePaintMinWidth: 1250
};
+47 -30
View File
@@ -2,7 +2,8 @@ const categorySeparator = '<sep gap="36"/>';
const blockSeparator = '<sep gap="36"/>'; // At default scale, about 28px
const motion = `
const motion = function (targetId) {
return `
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
<block type="motion_movesteps">
<value name="STEPS">
@@ -120,14 +121,16 @@ const motion = `
${blockSeparator}
<block type="motion_setrotationstyle"/>
${blockSeparator}
<block id="xposition" type="motion_xposition"/>
<block id="yposition" type="motion_yposition"/>
<block id="direction" type="motion_direction"/>
<block id="${targetId}_xposition" type="motion_xposition"/>
<block id="${targetId}_yposition" type="motion_yposition"/>
<block id="${targetId}_direction" type="motion_direction"/>
${categorySeparator}
</category>
`;
`;
};
const looks = `
const looks = function (targetId) {
return `
<category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
<block type="looks_sayforsecs">
<value name="MESSAGE">
@@ -229,15 +232,17 @@ const looks = `
</value>
</block>
${blockSeparator}
<block id="costumeorder" type="looks_costumeorder"/>
<block id="${targetId}_costumeorder" type="looks_costumeorder"/>
<block id="backdroporder" type="looks_backdroporder"/>
<block id="backdropname" type="looks_backdropname"/>
<block id="size" type="looks_size"/>
<block id="${targetId}_size" type="looks_size"/>
${categorySeparator}
</category>
`;
`;
};
const sound = `
const sound = function () {
return `
<category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
<block type="sound_play">
<value name="SOUND_MENU">
@@ -284,9 +289,11 @@ const sound = `
<block id="volume" type="sound_volume"/>
${categorySeparator}
</category>
`;
`;
};
const events = `
const events = function () {
return `
<category name="Events" colour="#FFD500" secondaryColour="#CC9900">
<block type="event_whenflagclicked"/>
<block type="event_whenkeypressed">
@@ -317,9 +324,11 @@ const events = `
</block>
${categorySeparator}
</category>
`;
`;
};
const control = `
const control = function () {
return `
<category name="Control" colour="#FFAB19" secondaryColour="#CF8B17">
<block type="control_wait">
<value name="DURATION">
@@ -354,9 +363,11 @@ const control = `
<block type="control_delete_this_clone"/>
${categorySeparator}
</category>
`;
`;
};
const sensing = `
const sensing = function () {
return `
<category name="Sensing" colour="#4CBFE6" secondaryColour="#2E8EB8">
<block type="sensing_touchingobject">
<value name="TOUCHINGOBJECTMENU">
@@ -422,9 +433,11 @@ const sensing = `
<block type="sensing_dayssince2000"/>
${categorySeparator}
</category>
`;
`;
};
const operators = `
const operators = function () {
return `
<category name="Operators" colour="#40BF4A" secondaryColour="#389438">
<block type="operator_add">
<value name="NUM1">
@@ -602,33 +615,37 @@ const operators = `
</block>
${categorySeparator}
</category>
`;
`;
};
const data = `
const data = function () {
return `
<category name="Data" colour="#FF8C1A" secondaryColour="#DB6E00" custom="VARIABLE">
</category>
`;
`;
};
const xmlOpen = '<xml style="display: none">';
const xmlClose = '</xml>';
/**
* @param {!string} targetId - The current editing target
* @param {string?} categoriesXML - null for default toolbox, or an XML string with <category> 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) {
+2 -2
View File
@@ -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) {
+18 -2
View File
@@ -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}')]`);
}
+5 -4
View File
@@ -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');
+18 -12
View File
@@ -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
@@ -4,6 +4,7 @@ exports[`IconButtonComponent matches snapshot 1`] = `
<div
className="custom-class-name"
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -341,6 +341,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -357,6 +358,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -373,6 +375,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -389,6 +392,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -405,6 +409,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -421,6 +426,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -437,6 +443,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
@@ -12,8 +12,10 @@ exports[`SpriteSelectorItemComponent matches snapshot when selected 1`] = `
onTouchStart={[Function]}
>
<div
aria-label="Close"
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}