同步至官方2020-02-29

This commit is contained in:
鸽子 2020-02-29 15:03:56 +08:00
parent ac62f4fcc1
commit c353eb6912
21 changed files with 15431 additions and 11519 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ npm-*
# generated translation files
/translations
/locale
node_modules

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -6,7 +6,7 @@
"scripts": {
"build": "npm run clean && webpack --progress --colors --bail",
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1) [skip ci]\"",
"prune": "./prune-gh-pages.sh",
"i18n:push": "tx-push-src scratch-editor interface translations/en.json",
"i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push",
@ -44,7 +44,7 @@
"babel-loader": "^8.0.4",
"base64-loader": "1.0.0",
"bowser": "1.9.4",
"chromedriver": "78.0.1",
"chromedriver": "80.0.0",
"classnames": "2.2.6",
"computed-style-to-inline-style": "3.0.0",
"copy-webpack-plugin": "^4.5.1",
@ -78,7 +78,7 @@
"lodash.pick": "4.4.0",
"lodash.throttle": "4.0.1",
"minilog": "3.1.0",
"mkdirp": "^0.5.1",
"mkdirp": "^1.0.3",
"omggif": "1.0.9",
"papaparse": "5.1.1",
"postcss-import": "^12.0.0",
@ -108,13 +108,13 @@
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20190925183642",
"scratch-l10n": "3.7.20191219145348",
"scratch-blocks": "0.1.0-prerelease.1576850350",
"scratch-paint": "0.2.0-prerelease.20191217213717",
"scratch-render": "0.1.0-prerelease.20191217212645",
"scratch-l10n": "3.7.20200225213156",
"scratch-blocks": "0.1.0-prerelease.1582033791",
"scratch-paint": "0.2.0-prerelease.20200213174123",
"scratch-render": "0.1.0-prerelease.20200228152431",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338",
"scratch-vm": "0.2.0-prerelease.20191227164934",
"scratch-svg-renderer": "0.2.0-prerelease.20200205003400",
"scratch-vm": "0.2.0-prerelease.20200227204654",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",

View File

@ -92,26 +92,21 @@ class LibraryItem extends React.PureComponent {
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
const iconMd5Prop = this.props.iconMd5;
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex] &&
this.props.icons[this.state.iconIndex].baseLayerMD5) {
return this.props.icons[this.state.iconIndex].baseLayerMD5;
this.state.iconIndex < this.props.icons.length) {
const icon = this.props.icons[this.state.iconIndex] || {};
return icon.md5ext || // 3.0 library format
icon.baseLayerMD5 || // 2.0 library format, TODO GH-5084
iconMd5Prop;
}
return this.props.iconMd5;
return iconMd5Prop;
}
render () {
const iconMd5 = this.curIconMd5();
let assetCDN
if('assetCDN' in window.scratchConfig){
assetCDN = window.scratchConfig.assetCDN+`/internalapi/asset/${iconMd5}`
}else{
assetCDN = `https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/`
}
const iconURL = iconMd5 ?
assetCDN :
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
this.props.iconRawURL;
return (
<LibraryItemComponent
@ -158,7 +153,8 @@ LibraryItem.propTypes = {
iconRawURL: PropTypes.string,
icons: PropTypes.arrayOf(
PropTypes.shape({
baseLayerMD5: PropTypes.string
baseLayerMD5: PropTypes.string, // 2.0 library format, TODO GH-5084
md5ext: PropTypes.string // 3.0 library format
})
),
id: PropTypes.number.isRequired,

View File

@ -102,7 +102,7 @@ const dropEveryOtherSample = buffer => {
}
return {
samples: newSamples,
sampleRate: buffer.rate / 2
sampleRate: buffer.sampleRate / 2
};
};

View File

@ -54,13 +54,10 @@ const cloudManagerHOC = function (WrappedComponent) {
canUseCloud (props) {
return !!(props.cloudHost && props.username && props.vm && props.projectId && props.hasCloudPermission);
}
shouldNotModifyCloudData (props) {
return (props.hasEverEnteredEditor && !props.canSave);
}
shouldConnect (props) {
return !this.isConnected() && this.canUseCloud(props) &&
props.isShowingWithId && props.vm.runtime.hasCloudData() &&
!this.shouldNotModifyCloudData(props);
props.canModifyCloudData;
}
shouldDisconnect (props, prevProps) {
return this.isConnected() &&
@ -70,7 +67,7 @@ const cloudManagerHOC = function (WrappedComponent) {
(props.projectId !== prevProps.projectId) ||
(props.username !== prevProps.username) ||
// Editing someone else's project
this.shouldNotModifyCloudData(props)
!props.canModifyCloudData
);
}
isConnected () {
@ -102,11 +99,11 @@ const cloudManagerHOC = function (WrappedComponent) {
render () {
const {
/* eslint-disable no-unused-vars */
canModifyCloudData,
cloudHost,
projectId,
username,
hasCloudPermission,
hasEverEnteredEditor,
isShowingWithId,
onShowCloudInfo,
/* eslint-enable no-unused-vars */
@ -124,23 +121,30 @@ const cloudManagerHOC = function (WrappedComponent) {
}
CloudManager.propTypes = {
canSave: PropTypes.bool.isRequired,
canModifyCloudData: PropTypes.bool.isRequired,
cloudHost: PropTypes.string,
hasCloudPermission: PropTypes.bool,
hasEverEnteredEditor: PropTypes.bool,
isShowingWithId: PropTypes.bool,
isShowingWithId: PropTypes.bool.isRequired,
onShowCloudInfo: PropTypes.func,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
username: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
const mapStateToProps = state => {
CloudManager.defaultProps = {
cloudHost: null,
hasCloudPermission: false,
onShowCloudInfo: () => {},
username: null
};
const mapStateToProps = (state, ownProps) => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
hasEverEnteredEditor: state.scratchGui.mode.hasEverEnteredEditor,
isShowingWithId: getIsShowingWithId(loadingState),
projectId: state.scratchGui.projectState.projectId
projectId: state.scratchGui.projectState.projectId,
// if you're editing someone else's project, you can't modify cloud data
canModifyCloudData: (!state.scratchGui.mode.hasEverEnteredEditor || ownProps.canSave)
};
};

View File

@ -178,7 +178,7 @@
},
{
"name": "Blue Sky 2 ",
"md5": "5a906c2f272b77f59f0ef207857a8b3a.svg",
"md5": "8eb8790be5507fdccf73e7c1570bbbab.svg",
"type": "backdrop",
"tags": [
"outdoors",
@ -286,7 +286,7 @@
},
{
"name": "Circles",
"md5": "4e29033ec2b891a8f1ca21242811d403.svg",
"md5": "c9847be305920807c5597d81576dd0c4.svg",
"type": "backdrop",
"tags": [
"patterns"
@ -491,7 +491,7 @@
},
{
"name": "Hearts",
"md5": "26d3418b2fbc1af2c5fea82e1c3df1db.svg",
"md5": "f98526ccb0eec3ac7d6c8f8ab502825e.svg",
"type": "backdrop",
"tags": [
"patterns"
@ -1050,7 +1050,7 @@
},
{
"name": "Underwater 1",
"md5": "fb907f72b310acc8b95cbf2d2cccabc9.svg",
"md5": "d3344650f594bcecdf46aa4a9441badd.svg",
"type": "backdrop",
"tags": [
"ocean",
@ -1175,7 +1175,7 @@
},
{
"name": "Witch House",
"md5": "597b9a9813fe5d8d387283138a0b8f2b.svg",
"md5": "30085b2d27beb5acdbe895d8b3e64b04.svg",
"type": "backdrop",
"tags": [
"fantasy",
@ -1190,7 +1190,7 @@
},
{
"name": "Woods",
"md5": "d26cfd278999c9e37d6af75c296a58df.svg",
"md5": "f3eb165d6f3fd23370f97079f2e631bf.svg",
"type": "backdrop",
"tags": [
"fantasy",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@ const randomizeSpritePosition = spriteObject => {
const randomY = Math.floor((100 * Math.random()) - 50);
if (spriteObject.hasOwnProperty('json')) {
// Library sprite object
spriteObject.json.scratchX = randomX;
spriteObject.json.scratchY = randomY;
spriteObject.json.x = randomX;
spriteObject.json.y = randomY;
} else if (spriteObject.hasOwnProperty('x') && spriteObject.hasOwnProperty('y')) {
// Scratch 3 sprite object
spriteObject.x = randomX;

View File

@ -26,7 +26,9 @@ const supportedBrowser = () => {
* always returns false
*/
const recommendedBrowser = () => !bowser.isUnsupportedBrowser(minVersions, true);
const recommendedBrowser = () =>
!bowser.isUnsupportedBrowser(minVersions, true) ||
window.navigator.userAgent.toLowerCase().indexOf('googlebot') !== -1;
export {
supportedBrowser as default,

View File

@ -3,7 +3,10 @@ import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {getIsShowingWithoutId} from '../reducers/project-state';
import {
getIsAnyCreatingNewState,
getIsShowingWithoutId
} from '../reducers/project-state';
import {setProjectTitle} from '../reducers/project-title';
const messages = defineMessages({
@ -42,6 +45,12 @@ const TitledHOC = function (WrappedComponent) {
if (this.props.projectTitle !== prevProps.projectTitle) {
this.handleReceivedProjectTitle(this.props.projectTitle);
}
// if project is a new default project, and has loaded,
if (this.props.isShowingWithoutId && prevProps.isAnyCreatingNewState) {
// reset title to default
const defaultProjectTitle = this.handleReceivedProjectTitle();
this.props.onUpdateProjectTitle(defaultProjectTitle);
}
// if the projectTitle hasn't changed, but the reduxProjectTitle
// HAS changed, we need to report that change to the projectTitle's owner
if (this.props.reduxProjectTitle !== prevProps.reduxProjectTitle &&
@ -55,11 +64,13 @@ const TitledHOC = function (WrappedComponent) {
newTitle = this.props.intl.formatMessage(messages.defaultProjectTitle);
}
this.props.onChangedProjectTitle(newTitle);
return newTitle;
}
render () {
const {
/* eslint-disable no-unused-vars */
intl,
isAnyCreatingNewState,
isShowingWithoutId,
onChangedProjectTitle,
// for children, we replace onUpdateProjectTitle with our own
@ -81,6 +92,7 @@ const TitledHOC = function (WrappedComponent) {
TitledComponent.propTypes = {
intl: intlShape,
isAnyCreatingNewState: PropTypes.bool,
isShowingWithoutId: PropTypes.bool,
onChangedProjectTitle: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
@ -95,6 +107,7 @@ const TitledHOC = function (WrappedComponent) {
const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
isAnyCreatingNewState: getIsAnyCreatingNewState(loadingState),
isShowingWithoutId: getIsShowingWithoutId(loadingState),
reduxProjectTitle: state.scratchGui.projectTitle
};

View File

@ -26,8 +26,7 @@ const vmListenerHOC = function (WrappedComponent) {
'handleKeyDown',
'handleKeyUp',
'handleProjectChanged',
'handleTargetsUpdate',
'handleRuntimeStarted'
'handleTargetsUpdate'
]);
// We have to start listening to the vm here rather than in
// componentDidMount because the HOC mounts the wrapped component,
@ -44,7 +43,6 @@ const vmListenerHOC = function (WrappedComponent) {
this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop);
this.props.vm.on('PROJECT_CHANGED', this.handleProjectChanged);
this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted);
this.props.vm.on('RUNTIME_STARTED', this.handleRuntimeStarted);
this.props.vm.on('PROJECT_START', this.props.onGreenFlag);
this.props.vm.on('PERIPHERAL_CONNECTION_LOST_ERROR', this.props.onShowExtensionAlert);
this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate);
@ -75,10 +73,6 @@ const vmListenerHOC = function (WrappedComponent) {
document.removeEventListener('keyup', this.handleKeyUp);
}
}
handleRuntimeStarted (){
window.props = this.props;
window.vm = this.props.vm;
}
handleProjectChanged () {
if (this.props.shouldUpdateProjectChanged && !this.props.projectChanged) {
this.props.onProjectChanged();

View File

@ -28,6 +28,8 @@ class SeleniumHelper {
'loadUri',
'rightClickText'
]);
this.Key = webdriver.Key; // map Key constants, for sending special keys
}
elementIsVisible (element, timeoutMessage = 'elementIsVisible timed out') {

View File

@ -193,7 +193,7 @@ describe('Working with the blocks', () => {
await clickText('newname', scope.blocksTab);
});
test('Renaming costume with a special character should not break toolbox', async () => {
test.skip('Renaming costume with a special character should not break toolbox', async () => {
await loadUri(uri);
// Rename the costume

View File

@ -193,7 +193,7 @@ describe('Working with costumes', () => {
.mouseMove(abbyElement)
.perform();
// wait for one of Abby's alternate costumes to appear
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]');
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/45de34b47a2ce22f6f5d28bb35a44ff5.svg/get/"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});

View File

@ -0,0 +1,45 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByXpath,
getDriver,
Key,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Project state', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('File->New resets project title', async () => {
const defaultProjectTitle = 'Scratch Project';
await loadUri(uri);
const inputEl = await findByXpath(`//input[@value="${defaultProjectTitle}"]`);
for (let i = 0; i < defaultProjectTitle.length; i++) {
inputEl.sendKeys(Key.BACK_SPACE);
}
inputEl.sendKeys('Changed title of project');
await clickText('Costumes'); // just to blur the input
// verify that project title has changed
await clickXpath('//input[@value="Changed title of project"]');
await clickXpath(
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
);
await clickXpath('//li[span[text()="New"]]');
// project title should be default again
await clickXpath(`//input[@value="${defaultProjectTitle}"]`);
});
});

View File

@ -87,11 +87,15 @@ describe('dropEveryOtherSample', () => {
sampleRate: 2
};
test('result is half the length', () => {
const {samples} = dropEveryOtherSample(buffer, 1);
const {samples} = dropEveryOtherSample(buffer);
expect(samples.length).toEqual(Math.floor(buffer.samples.length / 2));
});
test('result contains only even-index items', () => {
const {samples} = dropEveryOtherSample(buffer, 1);
const {samples} = dropEveryOtherSample(buffer);
expect(samples).toEqual(new Float32Array([1, 2, 3]));
});
test('result sampleRate is given sampleRate / 2', () => {
const {sampleRate} = dropEveryOtherSample(buffer);
expect(sampleRate).toEqual(buffer.sampleRate / 2);
});
});

View File

@ -60,7 +60,6 @@ describe('CloudManagerHOC', () => {
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -80,7 +79,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
store={store}
username="user"
@ -98,7 +96,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -115,7 +112,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={stillLoadingStore}
@ -132,7 +128,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
cloudHost="nonEmpty"
hasCloudPermission={false}
store={store}
@ -153,7 +148,6 @@ describe('CloudManagerHOC', () => {
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={stillLoadingStore}
@ -182,7 +176,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={stillLoadingStore}
@ -209,7 +202,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -235,7 +227,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -262,7 +253,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -293,7 +283,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -315,7 +304,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -343,7 +331,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -364,12 +351,11 @@ describe('CloudManagerHOC', () => {
});
// Editor Mode Connection/Disconnection Tests
test('Entering editor mode and can\'t save project should disconnect cloud provider # 1', () => {
test('Entering editor mode and can\'t save project should disconnect cloud provider', () => {
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@ -382,34 +368,7 @@ describe('CloudManagerHOC', () => {
const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection;
mounted.setProps({
canSave: false,
hasEverEnteredEditor: true
});
expect(vm.setCloudProvider.mock.calls.length).toBe(2);
expect(vm.setCloudProvider).toHaveBeenCalledWith(null);
expect(requestCloseConnection).toHaveBeenCalledTimes(1);
});
test('Entering editor mode and can\'t save project should disconnect cloud provider # 2', () => {
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
hasCloudPermission
canSave={false}
cloudHost="nonEmpty"
store={store}
username="user"
vm={vm}
/>
);
expect(CloudProvider).toHaveBeenCalled();
const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection;
mounted.setProps({
hasEverEnteredEditor: true
canModifyCloudData: false
});
expect(vm.setCloudProvider.mock.calls.length).toBe(2);