merge-upstream: initial merge of upstream/develop

installs & compiles but has test errors

aside from expected merge conflicts, notable changes from upstream:
 - removed husky, commitlint, etc.
 - removed semantic-release etc.
This commit is contained in:
Muffin committed 2023-12-22 17:25:21 -06:00
1 parent bd0cf91d85
commit 81a0382e0f
118 files changed
+33888 -5381

No files matched your search

+4
View File
@@ -9,3 +9,7 @@ trim_trailing_whitespace = true
[*.{js,html}]
indent_style = space
[.circleci/config.yml]
indent_size = 2
indent_style = space
+54
View File
@@ -0,0 +1,54 @@
name: CI/CD
on:
pull_request: # Runs whenever a pull request is created or updated
push: # Runs whenever a commit is pushed to the repository...
branches: [main, develop, hotfix/*] # ...on any of these branches
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
cancel-in-progress: true
permissions:
contents: write # publish a GitHub release
pages: write # deploy to GitHub Pages
issues: write # comment on released issues
pull-requests: write # comment on released pull requests
jobs:
ci-cd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: wagoid/commitlint-github-action@v5
if: github.event_name == 'pull_request'
- uses: actions/setup-node@v3
with:
cache: 'npm'
node-version-file: '.nvmrc'
- name: Info
run: |
cat <<EOF
Node version: $(node --version)
NPM version: $(npm --version)
GitHub ref: ${{ github.ref }}
GitHub head ref: ${{ github.head_ref }}
EOF
- name: Setup
run: |
npm ci
mkdir -p ./test/results
- run: npm test
- run: npm run build
- name: Deploy to GH Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: playground
full_commit_message: "Build for ${{ github.sha }} ${{ github.event.head_commit.message }}"
- name: semantic-release
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx --no -- semantic-release
+39
View File
@@ -0,0 +1,39 @@
name: Update i18n
on:
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
schedule:
- cron: 0 0 * * * # daily at midnight UTC = 7-8pm US Eastern
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
permissions:
contents: write
jobs:
ci-cd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
cache: 'npm'
node-version-file: '.nvmrc'
- name: Info
run: |
cat <<EOF
Node version: $(node --version)
NPM version: $(npm --version)
GitHub ref: ${{ github.ref }}
GitHub head ref: ${{ github.head_ref }}
EOF
- name: Setup
run: npm ci
- name: Extract translations
run: npm run i18n:src
- name: Push translations
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: npm run i18n:push
+1
View File
@@ -0,0 +1 @@
v16
-74
View File
@@ -1,74 +0,0 @@
language: node_js
node_js:
- lts/*
env:
global:
- NODE_ENV=production
- NPM_TAG=latest
- RELEASE_TIMESTAMP="$(date +'%Y%m%d%H%M%S')"
matrix:
- NPM_SCRIPT="tap:unit -- --jobs=4"
- NPM_SCRIPT="tap:integration -- --jobs=4"
cache:
directories:
- "$HOME/.npm"
install:
- npm ci --production=false
script: npm run $NPM_SCRIPT
jobs:
include:
- env: NPM_SCRIPT=lint
- env: NPM_SCRIPT=build
if: not (type != pull_request AND (branch =~ /^(develop|master|hotfix\/)/))
- stage: release
env: NPM_SCRIPT=build
before_deploy:
- >
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
VPKG=$($(npm bin)/json -f package.json version)
export RELEASE_VERSION=${VPKG}-prerelease.${RELEASE_TIMESTAMP}
npm --no-git-tag-version version $RELEASE_VERSION
if [[ "$TRAVIS_BRANCH" == hotfix/* ]]; then # double brackets are important for matching the wildcard
export NPM_TAG=hotfix
fi
git config --global user.email "$(git log --pretty=format:"%ae" -n1)"
git config --global user.name "$(git log --pretty=format:"%an" -n1)"
export BEFORE_DEPLOY_RAN=true
fi
deploy:
- provider: npm
on:
branch:
- master
- develop
- hotfix/*
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
email: $NPM_EMAIL
api_key: $NPM_TOKEN
tag: $NPM_TAG
- provider: script
on:
branch:
- master
- develop
- hotfix/*
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: if npm info | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi
- provider: script
on:
all_branches: true
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: npm run --silent deploy -- -x -r $GH_PAGES_REPO
- provider: script
on:
branch: develop
condition: $TRAVIS_EVENT_TYPE == cron
skip_cleanup: true
script: npm run i18n:src && npm run i18n:push
stages:
- test
- name: release
if: type != pull_request AND (branch =~ /^(develop|master|hotfix\/)/)
+2615
View File
File diff suppressed because it is too large. Load diff
+5 -6
View File
@@ -27,10 +27,9 @@ This section was too out of date to be useful. We hope to re-add it as some poin
<!--
## scratch-vm
#### Scratch VM is a library for representing, running, and maintaining the state of computer programs written using [Scratch Blocks](https://github.com/LLK/scratch-blocks).
#### Scratch VM is a library for representing, running, and maintaining the state of computer programs written using [Scratch Blocks](https://github.com/scratchfoundation/scratch-blocks).
[![Build Status](https://travis-ci.org/LLK/scratch-vm.svg?branch=develop)](https://travis-ci.org/LLK/scratch-vm)
[![Coverage Status](https://coveralls.io/repos/github/LLK/scratch-vm/badge.svg?branch=develop)](https://coveralls.io/github/LLK/scratch-vm?branch=develop)
[![CI/CD](https://github.com/scratchfoundation/scratch-vm/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/scratchfoundation/scratch-vm/actions/workflows/ci-cd.yml)
## Installation
This requires you to have Git and Node.js installed.
@@ -41,7 +40,7 @@ npm install scratch-vm
```
To set up a development environment to edit scratch-vm yourself:
```bash
git clone https://github.com/LLK/scratch-vm.git
git clone https://github.com/scratchfoundation/scratch-vm.git
cd scratch-vm
npm install
```
@@ -49,7 +48,7 @@ npm install
## Development Server
This requires Node.js to be installed.
For convenience, we've included a development server with the VM. This is sometimes useful when running in an environment that's loading remote resources (e.g., SVGs from the Scratch server). If you would like to use your modified VM with the full Scratch 3.0 GUI, [follow the instructions to link the VM to the GUI](https://github.com/LLK/scratch-gui/wiki/Getting-Started).
For convenience, we've included a development server with the VM. This is sometimes useful when running in an environment that's loading remote resources (e.g., SVGs from the Scratch server). If you would like to use your modified VM with the full Scratch 3.0 GUI, [follow the instructions to link the VM to the GUI](https://github.com/scratchfoundation/scratch-gui/wiki/Getting-Started).
## Running the Development Server
Open a Command Prompt or Terminal in the repository and run:
@@ -92,7 +91,7 @@ vm.start();
## Abstract Syntax Tree
#### Overview
The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/LLK/scratch-blocks) workspace via the `blockListener`. Each target (code-running object, for example, a sprite) keeps an AST for its blocks. At any time, the current state of an AST can be viewed by inspecting the `vm.runtime.targets[...].blocks` object.
The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/scratchfoundation/scratch-blocks) workspace via the `blockListener`. Each target (code-running object, for example, a sprite) keeps an AST for its blocks. At any time, the current state of an AST can be viewed by inspecting the `vm.runtime.targets[...].blocks` object.
#### Anatomy of a Block
The VM's block representation contains all the important information for execution and storage. Here's an example representing the "when key pressed" script on a workspace:
+1 -1
View File
@@ -48,7 +48,7 @@ ways.
## Defining an Extension
Scratch extensions are defined as a single Javascript class which accepts either a reference to the Scratch
[VM](https://github.com/llk/scratch-vm) runtime or a "runtime proxy" which handles communication with the Scratch VM
[VM](https://github.com/scratchfoundation/scratch-vm) runtime or a "runtime proxy" which handles communication with the Scratch VM
across a well defined worker boundary (i.e. the sandbox).
```js
+30470 -4857
View File
File diff suppressed because it is too large. Load diff
+26 -21
View File
@@ -1,33 +1,38 @@
{
"name": "scratch-vm",
"version": "0.2.0",
"version": "2.1.46",
"description": "Virtual Machine for Scratch 3.0",
"author": "Massachusetts Institute of Technology",
"license": "BSD-3-Clause",
"homepage": "https://github.com/LLK/scratch-vm#readme",
"homepage": "https://github.com/scratchfoundation/scratch-vm#readme",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-vm.git"
"url": "https://github.com/scratchfoundation/scratch-vm.git"
},
"main": "./src/index.js",
"browser": "./src/index.js",
"scripts": {
"build": "npm run docs && webpack --progress --colors --bail",
"coverage": "tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
"deploy": "touch playground/.nojekyll && gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
"docs": "exit 0 || jsdoc -c .jsdoc.json",
"docs": "jsdoc -c .jsdoc.json",
"i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js",
"i18n:push": "tx-push-src scratch-editor extensions translations/core/en.json",
"lint": "exit 0 || eslint . && format-message lint src/**/*.js",
"prepublishOnly": "in-publish && npm run build || not-in-publish",
"lint": "eslint . && format-message lint src/**/*.js",
"prepublish": "in-publish && npm run build || not-in-publish",
"start": "webpack-dev-server",
"tap": "tap ./test/{unit,integration}/*.js",
"tap:unit": "tap ./test/unit/*.js",
"tap:integration": "tap ./test/integration/*.js",
"test": "npm run lint && npm run docs && npm run tap",
"test": "npm run lint && npm run tap",
"watch": "webpack --progress --colors --watch",
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
},
"tap": {
"branches": 60,
"functions": 70,
"lines": 70,
"statements": 70
},
"dependencies": {
"@turbowarp/json": "^0.1.2",
"@vernier/godirect": "1.5.0",
@@ -39,13 +44,14 @@
"diff-match-patch": "1.0.4",
"format-message": "6.2.1",
"htmlparser2": "3.10.0",
"immutable": "3.8.2",
"immutable": "3.8.1",
"jszip": "^3.1.5",
"minilog": "3.1.0",
"scratch-parser": "git+https://github.com/TurboWarp/scratch-parser.git#72f2cde00fc968ce40282e0392d3553963730748",
"scratch-parser": "5.1.1",
"scratch-sb1-converter": "0.2.7",
"scratch-translate-extension-languages": "0.0.20191118205314",
"text-encoding": "0.7.0",
"uuid": "8.3.2",
"worker-loader": "^1.1.1"
},
"peerDependencies": {
@@ -60,28 +66,27 @@
"callsite": "1.0.0",
"copy-webpack-plugin": "4.5.4",
"docdash": "1.2.0",
"eslint": "5.3.0",
"eslint-config-scratch": "5.1.0",
"eslint": "8.55.0",
"eslint-config-scratch": "9.0.3",
"expose-loader": "0.7.5",
"file-loader": "2.0.0",
"format-message-cli": "6.2.0",
"gh-pages": "1.2.0",
"in-publish": "2.0.1",
"jsdoc": "3.6.6",
"js-md5": "0.7.3",
"jsdoc": "3.6.6",
"json": "^9.0.4",
"lodash.defaultsdeep": "4.6.1",
"pngjs": "3.3.3",
"scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20220524062105",
"scratch-l10n": "3.14.20220526031602",
"scratch-render": "0.1.0-prerelease.20211028200436",
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
"scratch-storage": "2.0.2",
"scratch-audio": "0.1.0-prerelease.20231221012053",
"scratch-blocks": "0.1.0-prerelease.20230527085947",
"scratch-l10n": "3.16.20231222031921",
"scratch-render": "0.1.0-prerelease.20231220210403",
"scratch-render-fonts": "1.0.0-prerelease.20231017225105",
"scratch-storage": "2.3.1",
"scratch-svg-renderer": "0.2.0-prerelease.20210727023023",
"script-loader": "0.7.2",
"stats.js": "0.17.0",
"tap": "12.0.1",
"tap": "16.2.0",
"tiny-worker": "2.3.0",
"uglifyjs-webpack-plugin": "1.2.7",
"webpack": "4.46.0",
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
extends: 'scratch-semantic-release-config',
branches: [
{
name: 'develop'
// default channel
},
{
name: 'hotfix/*',
channel: 'hotfix'
}
]
};
+2 -2
View File
@@ -249,7 +249,7 @@ class Scratch3LooksBlocks {
// Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that
// rounding would display them as 0.00. This matches 2.0's behavior:
// https://github.com/LLK/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585
// https://github.com/scratchfoundation/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/scratch/ScratchSprite.as#L579-L585
if (typeof text === 'number' &&
Math.abs(text) >= 0.01 && text % 1 !== 0) {
text = text.toFixed(2);
@@ -543,7 +543,7 @@ class Scratch3LooksBlocks {
changeEffect (args, util) {
const effect = Cast.toString(args.EFFECT).toLowerCase();
const change = Cast.toNumber(args.CHANGE);
if (!util.target.effects.hasOwnProperty(effect)) return;
if (!Object.prototype.hasOwnProperty.call(util.target.effects, effect)) return;
let newValue = change + util.target.effects[effect];
newValue = this.clampEffect(effect, newValue);
util.target.setEffect(effect, newValue);
+1 -1
View File
@@ -63,7 +63,7 @@ class Scratch3ProcedureBlocks {
// at earlier stack frames for the values of a given parameter (#1729)
util.initParams();
for (let i = 0; i < paramIds.length; i++) {
if (args.hasOwnProperty(paramIds[i])) {
if (Object.prototype.hasOwnProperty.call(args, paramIds[i])) {
util.pushParam(paramNames[i], args[paramIds[i]]);
} else {
util.pushParam(paramNames[i], paramDefaults[i]);
+2 -2
View File
@@ -278,7 +278,7 @@ class Scratch3SoundBlocks {
const value = Cast.toNumber(args.VALUE);
const soundState = this._getSoundState(util.target);
if (!soundState.effects.hasOwnProperty(effect)) return;
if (!Object.prototype.hasOwnProperty.call(soundState.effects, effect)) return;
if (change) {
soundState.effects[effect] += value;
@@ -317,7 +317,7 @@ class Scratch3SoundBlocks {
_clearEffectsForTarget (target) {
const soundState = this._getSoundState(target);
for (const effect in soundState.effects) {
if (!soundState.effects.hasOwnProperty(effect)) continue;
if (!Object.prototype.hasOwnProperty.call(soundState.effects, effect)) continue;
soundState.effects[effect] = 0;
}
this._syncEffectsForTarget(target);
+3 -1
View File
@@ -50,6 +50,8 @@ class CentralDispatch extends SharedDispatch {
throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`);
}
// TODO: verify correct `this` after switching from apply to spread
// eslint-disable-next-line prefer-spread
return provider[method].apply(provider, args);
}
throw new Error(`Provider not found for service: ${service}`);
@@ -62,7 +64,7 @@ class CentralDispatch extends SharedDispatch {
* @param {object} provider - a local object which provides this service.
*/
setServiceSync (service, provider) {
if (this.services.hasOwnProperty(service)) {
if (Object.prototype.hasOwnProperty.call(this.services, service)) {
log.warn(`Central dispatch replacing existing service provider for ${service}`);
}
this.services[service] = provider;
+2
View File
@@ -82,6 +82,8 @@ class SharedDispatch {
return this._remoteTransferCall(provider, service, method, transfer, ...args);
}
// TODO: verify correct `this` after switching from apply to spread
// eslint-disable-next-line prefer-spread
const result = provider[method].apply(provider, args);
return Promise.resolve(result);
}
+1 -1
View File
@@ -59,7 +59,7 @@ class WorkerDispatch extends SharedDispatch {
* @returns {Promise} - a promise which will resolve once the service is registered.
*/
setService (service, provider) {
if (this.services.hasOwnProperty(service)) {
if (Object.prototype.hasOwnProperty.call(this.services, service)) {
log.warn(`Worker dispatch replacing existing service provider for ${service}`);
}
this.services[service] = provider;
+1 -1
View File
@@ -153,7 +153,7 @@ const domToBlocks = function (blocksDOM) {
// Flatten blocks object into a list.
const blocksList = [];
for (const b in blocks) {
if (!blocks.hasOwnProperty(b)) continue;
if (!Object.prototype.hasOwnProperty.call(blocks, b)) continue;
blocksList.push(blocks[b]);
}
return blocksList;
+2
View File
@@ -232,6 +232,8 @@ class BlockUtility {
this.sequencer.runtime.ioDevices[device] &&
this.sequencer.runtime.ioDevices[device][func]) {
const devObject = this.sequencer.runtime.ioDevices[device];
// TODO: verify correct `this` after switching from apply to spread
// eslint-disable-next-line prefer-spread
return devObject[func].apply(devObject, args);
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ class RuntimeScriptCache {
if (Object.keys(fields).length === 0) {
const inputs = container.getInputs(block);
for (const input in inputs) {
if (!inputs.hasOwnProperty(input)) continue;
if (!Object.prototype.hasOwnProperty.call(inputs, input)) continue;
const id = inputs[input].block;
const inputBlock = container.getBlock(id);
const inputFields = container.getFields(inputBlock);
+31 -22
View File
@@ -284,7 +284,7 @@ class Blocks {
}
for (const id in this._blocks) {
if (!this._blocks.hasOwnProperty(id)) continue;
if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue;
const block = this._blocks[id];
if (block.opcode === 'procedures_definition') {
// tw: make sure that populateProcedureCache is kept up to date with this method
@@ -321,7 +321,7 @@ class Blocks {
}
for (const id in this._blocks) {
if (!this._blocks.hasOwnProperty(id)) continue;
if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue;
const block = this._blocks[id];
if (block.opcode === 'procedures_prototype' &&
block.mutation.proccode === name) {
@@ -455,7 +455,7 @@ class Blocks {
case 'delete':
// Don't accept delete events for missing blocks,
// or shadow blocks being obscured.
if (!this._blocks.hasOwnProperty(e.blockId) ||
if (!Object.prototype.hasOwnProperty.call(this._blocks, e.blockId) ||
this._blocks[e.blockId].shadow) {
return;
}
@@ -496,7 +496,7 @@ class Blocks {
}
break;
case 'var_rename':
if (editingTarget && editingTarget.variables.hasOwnProperty(e.varId)) {
if (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) {
// This is a local variable, rename on the current target
editingTarget.renameVariable(e.varId, e.newName);
// Update all the blocks on the current target that use
@@ -516,7 +516,7 @@ class Blocks {
break;
case 'var_delete': {
this.resetCache(); // tw: more aggressive cache resetting
const target = (editingTarget && editingTarget.variables.hasOwnProperty(e.varId)) ?
const target = (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) ?
editingTarget : stage;
target.deleteVariable(e.varId);
this.emitProjectChanged();
@@ -547,20 +547,21 @@ class Blocks {
this.resetCache(); // tw: comments can affect compilation
if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget();
if (!currTarget.comments.hasOwnProperty(e.commentId)) {
if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`);
return;
}
const comment = currTarget.comments[e.commentId];
const change = e.newContents_;
if (change.hasOwnProperty('minimized')) {
if (Object.prototype.hasOwnProperty.call(change, 'minimized')) {
comment.minimized = change.minimized;
}
if (change.hasOwnProperty('width') && change.hasOwnProperty('height')){
if (Object.prototype.hasOwnProperty.call(change, 'width') &&
Object.prototype.hasOwnProperty.call(change, 'height')) {
comment.width = change.width;
comment.height = change.height;
}
if (change.hasOwnProperty('text')) {
if (Object.prototype.hasOwnProperty.call(change, 'text')) {
comment.text = change.text;
}
this.emitProjectChanged();
@@ -569,7 +570,7 @@ class Blocks {
case 'comment_move':
if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget();
if (currTarget && !currTarget.comments.hasOwnProperty(e.commentId)) {
if (currTarget && !Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`);
return;
}
@@ -585,7 +586,7 @@ class Blocks {
this.resetCache(); // tw: comments can affect compilation
if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget();
if (!currTarget.comments.hasOwnProperty(e.commentId)) {
if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
// If we're in this state, we have probably received
// a delete event from a workspace that we switched from
// (e.g. a delete event for a comment on sprite a's workspace
@@ -642,7 +643,7 @@ class Blocks {
createBlock (block) {
// Does the block already exist?
// Could happen, e.g., for an unobscured shadow.
if (this._blocks.hasOwnProperty(block.id)) {
if (Object.prototype.hasOwnProperty.call(this._blocks, block.id)) {
return;
}
// Create new block.
@@ -756,7 +757,7 @@ class Blocks {
}
const isSpriteSpecific = isSpriteLocalVariable ||
(this.runtime.monitorBlockInfo.hasOwnProperty(block.opcode) &&
(Object.prototype.hasOwnProperty.call(this.runtime.monitorBlockInfo, block.opcode) &&
this.runtime.monitorBlockInfo[block.opcode].isSpriteSpecific);
if (isSpriteSpecific) {
// If creating a new sprite specific monitor, the only possible target is
@@ -798,7 +799,7 @@ class Blocks {
* @param {!object} e Blockly move event to be processed
*/
moveBlock (e) {
if (!this._blocks.hasOwnProperty(e.id)) {
if (!Object.prototype.hasOwnProperty.call(this._blocks, e.id)) {
return;
}
@@ -846,7 +847,7 @@ class Blocks {
// Moved to the new parent's input.
// Don't obscure the shadow block.
let oldShadow = null;
if (this._blocks[e.newParent].inputs.hasOwnProperty(e.newInput)) {
if (Object.prototype.hasOwnProperty.call(this._blocks[e.newParent].inputs, e.newInput)) {
oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
}
@@ -937,6 +938,14 @@ class Blocks {
this.emitProjectChanged();
}
/**
* Delete all blocks and their associated scripts.
*/
deleteAllBlocks () {
const blockIds = Object.keys(this._blocks);
blockIds.forEach(blockId => this.deleteBlock(blockId));
}
/**
* Returns a map of all references to variables or lists from blocks
* in this block container.
@@ -1089,7 +1098,7 @@ class Blocks {
*/
_getCostumeField (blockId) {
const block = this.getBlock(blockId);
if (block && block.fields.hasOwnProperty('COSTUME')) {
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'COSTUME')) {
return block.fields.COSTUME;
}
return null;
@@ -1104,7 +1113,7 @@ class Blocks {
*/
_getSoundField (blockId) {
const block = this.getBlock(blockId);
if (block && block.fields.hasOwnProperty('SOUND_MENU')) {
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'SOUND_MENU')) {
return block.fields.SOUND_MENU;
}
return null;
@@ -1119,7 +1128,7 @@ class Blocks {
*/
_getBackdropField (blockId) {
const block = this.getBlock(blockId);
if (block && block.fields.hasOwnProperty('BACKDROP')) {
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'BACKDROP')) {
return block.fields.BACKDROP;
}
return null;
@@ -1141,7 +1150,7 @@ class Blocks {
'DISTANCETOMENU', 'TOUCHINGOBJECTMENU', 'CLONE_OPTION'];
for (let i = 0; i < spriteMenuNames.length; i++) {
const menuName = spriteMenuNames[i];
if (block.fields.hasOwnProperty(menuName)) {
if (Object.prototype.hasOwnProperty.call(block.fields, menuName)) {
return block.fields[menuName];
}
}
@@ -1184,7 +1193,7 @@ class Blocks {
const commentId = block.comment;
if (commentId) {
if (comments) {
if (comments.hasOwnProperty(commentId)) {
if (Object.prototype.hasOwnProperty.call(comments, commentId)) {
xmlString += comments[commentId].toXML();
} else {
log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`);
@@ -1199,7 +1208,7 @@ class Blocks {
}
// Add any inputs on this block.
for (const input in block.inputs) {
if (!block.inputs.hasOwnProperty(input)) continue;
if (!Object.prototype.hasOwnProperty.call(block.inputs, input)) continue;
const blockInput = block.inputs[input];
// Only encode a value tag if the value input is occupied.
if (blockInput.block || blockInput.shadow) {
@@ -1216,7 +1225,7 @@ class Blocks {
}
// Add any fields on this block.
for (const field in block.fields) {
if (!block.fields.hasOwnProperty(field)) continue;
if (!Object.prototype.hasOwnProperty.call(block.fields, field)) continue;
const blockField = block.fields[field];
xmlString += `<field name="${xmlEscape(blockField.name)}"`;
const fieldId = blockField.id;
+88 -20
View File
@@ -1,6 +1,7 @@
const EventEmitter = require('events');
const {OrderedMap} = require('immutable');
const ExtendedJSON = require('@turbowarp/json');
const uuid = require('uuid');
const ArgumentType = require('../extension-support/argument-type');
const Blocks = require('./blocks');
@@ -20,6 +21,7 @@ const Variable = require('./variable');
const xmlEscape = require('../util/xml-escape');
const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
const FontManager = require('./tw-font-manager');
const fetchWithTimeout = require('../util/fetch-with-timeout');
// Virtual I/O devices.
const Clock = require('../io/clock');
@@ -437,6 +439,10 @@ class Runtime extends EventEmitter {
*/
this.origin = null;
this._initScratchLink();
this.resetRunId();
this._stageTarget = null;
this.addonBlocks = {};
@@ -940,14 +946,14 @@ class Runtime extends EventEmitter {
*/
_registerBlockPackages () {
for (const packageName in defaultBlockPackages) {
if (defaultBlockPackages.hasOwnProperty(packageName)) {
if (Object.prototype.hasOwnProperty.call(defaultBlockPackages, packageName)) {
// @todo pass a different runtime depending on package privilege?
const packageObject = new (defaultBlockPackages[packageName])(this);
// Collect primitives from package.
if (packageObject.getPrimitives) {
const packagePrimitives = packageObject.getPrimitives();
for (const op in packagePrimitives) {
if (packagePrimitives.hasOwnProperty(op)) {
if (Object.prototype.hasOwnProperty.call(packagePrimitives, op)) {
this._primitives[op] =
packagePrimitives[op].bind(packageObject);
}
@@ -957,7 +963,7 @@ class Runtime extends EventEmitter {
if (packageObject.getHats) {
const packageHats = packageObject.getHats();
for (const hatName in packageHats) {
if (packageHats.hasOwnProperty(hatName)) {
if (Object.prototype.hasOwnProperty.call(packageHats, hatName)) {
this._hats[hatName] = packageHats[hatName];
}
}
@@ -1033,7 +1039,7 @@ class Runtime extends EventEmitter {
this._fillExtensionCategory(categoryInfo, extensionInfo);
for (const fieldTypeName in categoryInfo.customFieldTypes) {
if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) {
if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) {
const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName];
// Emit events for custom field types from extension
@@ -1076,7 +1082,7 @@ class Runtime extends EventEmitter {
categoryInfo.menuInfo = {};
for (const menuName in extensionInfo.menus) {
if (extensionInfo.menus.hasOwnProperty(menuName)) {
if (Object.prototype.hasOwnProperty.call(extensionInfo.menus, menuName)) {
const menuInfo = extensionInfo.menus[menuName];
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo);
categoryInfo.menus.push(convertedMenu);
@@ -1084,7 +1090,7 @@ class Runtime extends EventEmitter {
}
}
for (const fieldTypeName in extensionInfo.customFieldTypes) {
if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) {
if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) {
const fieldType = extensionInfo.customFieldTypes[fieldTypeName];
const fieldTypeInfo = this._buildCustomFieldInfo(
fieldTypeName,
@@ -1354,7 +1360,7 @@ class Runtime extends EventEmitter {
break;
case BlockType.HAT:
case BlockType.EVENT:
if (!blockInfo.hasOwnProperty('isEdgeActivated')) {
if (!Object.prototype.hasOwnProperty.call(blockInfo, 'isEdgeActivated')) {
// if absent, this property defaults to true
blockInfo.isEdgeActivated = true;
}
@@ -1695,6 +1701,38 @@ class Runtime extends EventEmitter {
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
}
/**
* One-time initialization for Scratch Link support.
*/
_initScratchLink () {
// Check that we're actually in a real browser, not Node.js or JSDOM, and we have a valid-looking origin.
// note that `if (self?....)` will throw if `self` is undefined, so check for that first!
if (typeof self !== 'undefined' &&
typeof document !== 'undefined' &&
document.getElementById &&
self.origin &&
self.origin !== 'null' && // note this is a string comparison, not a null check
self.navigator &&
self.navigator.userAgent &&
!(
self.navigator.userAgent.includes('Node.js') ||
self.navigator.userAgent.includes('jsdom')
)
) {
// Create a script tag for the Scratch Link browser extension, unless one already exists
const scriptElement = document.getElementById('scratch-link-extension-script');
if (!scriptElement) {
const script = document.createElement('script');
script.id = 'scratch-link-extension-script';
document.body.appendChild(script);
// Tell the browser extension to inject its script.
// If the extension isn't present or isn't active, this will do nothing.
self.postMessage('inject-scratch-link-script', self.origin);
}
}
}
/**
* Get a scratch link socket.
* @param {string} type Either BLE or BT
@@ -1720,7 +1758,11 @@ class Runtime extends EventEmitter {
* @returns {ScratchLinkSocket} The new scratch link socket (a WebSocket object)
*/
_defaultScratchLinkSocketFactory (type) {
return new ScratchLinkWebSocket(type);
const Scratch = self.Scratch;
const ScratchLinkSafariSocket = Scratch && Scratch.ScratchLinkSafariSocket;
// detect this every time in case the user turns on the extension after loading the page
const useSafariSocket = ScratchLinkSafariSocket && ScratchLinkSafariSocket.isSafariHelperCompatible();
return useSafariSocket ? new ScratchLinkSafariSocket(type) : new ScratchLinkWebSocket(type);
}
/**
@@ -1800,7 +1842,7 @@ class Runtime extends EventEmitter {
* @return {boolean} True if the op is known to be a hat.
*/
getIsHat (opcode) {
return this._hats.hasOwnProperty(opcode);
return Object.prototype.hasOwnProperty.call(this._hats, opcode);
}
/**
@@ -1809,7 +1851,7 @@ class Runtime extends EventEmitter {
* @return {boolean} True if the op is known to be a edge-activated hat.
*/
getIsEdgeActivatedHat (opcode) {
return this._hats.hasOwnProperty(opcode) &&
return Object.prototype.hasOwnProperty.call(this._hats, opcode) &&
this._hats[opcode].edgeActivated;
}
@@ -1870,6 +1912,9 @@ class Runtime extends EventEmitter {
);
};
}
fetchWithTimeout.setFetch(storage.scratchFetch.scratchFetch);
this.resetRunId();
}
// -----------------------------------------------------------------------------
@@ -2075,7 +2120,7 @@ class Runtime extends EventEmitter {
*/
startHats (requestedHatOpcode,
optMatchFields, optTarget) {
if (!this._hats.hasOwnProperty(requestedHatOpcode)) {
if (!Object.prototype.hasOwnProperty.call(this._hats, requestedHatOpcode)) {
// No known hat with this opcode.
return;
}
@@ -2085,7 +2130,7 @@ class Runtime extends EventEmitter {
const hatMeta = instance._hats[requestedHatOpcode];
for (const opts in optMatchFields) {
if (!optMatchFields.hasOwnProperty(opts)) continue;
if (!Object.prototype.hasOwnProperty.call(optMatchFields, opts)) continue;
optMatchFields[opts] = optMatchFields[opts].toUpperCase();
}
@@ -2304,6 +2349,19 @@ class Runtime extends EventEmitter {
}
}
/**
* Reset the Run ID. Call this any time the project logically starts, stops, or changes identity.
*/
resetRunId () {
if (!this.storage) {
// see also: attachStorage
return;
}
const newRunId = uuid.v1();
this.storage.scratchFetch.setMetadata(this.storage.scratchFetch.RequestMetadata.RunId, newRunId);
}
/**
* Start all threads that start with the green flag.
*/
@@ -2331,7 +2389,7 @@ class Runtime extends EventEmitter {
const newTargets = [];
for (let i = 0; i < this.targets.length; i++) {
this.targets[i].onStopAll();
if (this.targets[i].hasOwnProperty('isOriginal') &&
if (Object.prototype.hasOwnProperty.call(this.targets[i], 'isOriginal') &&
!this.targets[i].isOriginal) {
this.targets[i].dispose();
} else {
@@ -2346,6 +2404,8 @@ class Runtime extends EventEmitter {
// Remove all remaining threads from executing in the next tick.
this.threads = [];
this.threadMap.clear();
this.resetRunId();
}
_renderInterpolatedPositions () {
@@ -2392,7 +2452,7 @@ class Runtime extends EventEmitter {
// Find all edge-activated hats, and add them to threads to be evaluated.
for (const hatType in this._hats) {
if (!this._hats.hasOwnProperty(hatType)) continue;
if (!Object.prototype.hasOwnProperty.call(this._hats, hatType)) continue;
const hat = this._hats[hatType];
if (hat.edgeActivated) {
this.startHats(hatType);
@@ -2821,9 +2881,9 @@ class Runtime extends EventEmitter {
*/
_updateGlows (optExtraThreads) {
const searchThreads = [];
searchThreads.push.apply(searchThreads, this.threads);
searchThreads.push(...this.threads);
if (optExtraThreads) {
searchThreads.push.apply(searchThreads, optExtraThreads);
searchThreads.push(...optExtraThreads);
}
// Set of scripts that request a glow this frame.
const requestedGlowsThisFrame = [];
@@ -3091,10 +3151,11 @@ class Runtime extends EventEmitter {
}
/**
* Report that the project has loaded in the Virtual Machine.
* Handle that the project has loaded in the Virtual Machine.
*/
emitProjectLoaded () {
handleProjectLoaded () {
this.emit(Runtime.PROJECT_LOADED);
this.resetRunId();
}
/**
@@ -3244,10 +3305,17 @@ class Runtime extends EventEmitter {
}
/**
* tw: Stop the tick loop
* Note: This only stops the loop. It will not stop any threads the next time the VM starts
* @deprecated Used by old versions of TurboWarp. Superceded by upstream's quit()
*/
stop () {
this.quit();
}
/**
* Quit the Runtime, clearing any handles which might keep the process alive.
* Do not use the runtime after calling this method. This method is meant for test shutdown.
*/
quit () {
if (!this.frameLoop.running) {
return;
}
+10 -10
View File
@@ -101,7 +101,7 @@ class Target extends EventEmitter {
}
hasEdgeActivatedValue (blockId) {
return this._edgeActivatedHatValues.hasOwnProperty(blockId);
return Object.prototype.hasOwnProperty.call(this._edgeActivatedHatValues, blockId);
}
/**
@@ -186,13 +186,13 @@ class Target extends EventEmitter {
*/
lookupVariableById (id) {
// If we have a local copy, return it.
if (this.variables.hasOwnProperty(id)) {
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
return this.variables[id];
}
// If the stage has a global copy, return it.
if (this.runtime && !this.isStage) {
const stage = this.runtime.getTargetForStage();
if (stage && stage.variables.hasOwnProperty(id)) {
if (stage && Object.prototype.hasOwnProperty.call(stage.variables, id)) {
return stage.variables[id];
}
}
@@ -264,7 +264,7 @@ class Target extends EventEmitter {
* Additional checks are made that the variable can be created as a cloud variable.
*/
createVariable (id, name, type, isCloud) {
if (!this.variables.hasOwnProperty(id)) {
if (!Object.prototype.hasOwnProperty.call(this.variables, id)) {
const newVariable = new Variable(id, name, type, false);
if (isCloud && this.isStage && this.runtime.canAddCloudVariable()) {
newVariable.isCloud = true;
@@ -288,7 +288,7 @@ class Target extends EventEmitter {
* @param {boolean} minimized Whether the comment is minimized.
*/
createComment (id, blockId, text, x, y, width, height, minimized) {
if (!this.comments.hasOwnProperty(id)) {
if (!Object.prototype.hasOwnProperty.call(this.comments, id)) {
const newComment = new Comment(id, text, x, y,
width, height, minimized);
if (blockId) {
@@ -311,7 +311,7 @@ class Target extends EventEmitter {
* @param {string} newName New name for the variable.
*/
renameVariable (id, newName) {
if (this.variables.hasOwnProperty(id)) {
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
const variable = this.variables[id];
if (variable.id === id) {
const oldName = variable.name;
@@ -362,7 +362,7 @@ class Target extends EventEmitter {
* @param {string} id Id of variable to delete.
*/
deleteVariable (id) {
if (this.variables.hasOwnProperty(id)) {
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
// Get info about the variable before deleting it
const deletedVariableName = this.variables[id].name;
const deletedVariableWasCloud = this.variables[id].isCloud;
@@ -408,7 +408,7 @@ class Target extends EventEmitter {
* the original variable was not found.
*/
duplicateVariable (id, optKeepOriginalId) {
if (this.variables.hasOwnProperty(id)) {
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
const originalVariable = this.variables[id];
const newVariable = new Variable(
optKeepOriginalId ? id : null, // conditionally keep original id or generate a new one
@@ -695,7 +695,7 @@ class Target extends EventEmitter {
const unreferencedLocalVarIds = [];
if (Object.keys(this.variables).length > 0) {
for (const localVarId in this.variables) {
if (!this.variables.hasOwnProperty(localVarId)) continue;
if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
}
}
@@ -720,7 +720,7 @@ class Target extends EventEmitter {
if (this.lookupVariableById(varId)) {
// Found a variable with the id in either the target or the stage,
// figure out which one.
if (this.variables.hasOwnProperty(varId)) {
if (Object.prototype.hasOwnProperty.call(this.variables, varId)) {
// If the target has the variable, then check whether the stage
// has one with the same name and type. If it does, then rename
// this target specific variable so that there is a distinction.
+1 -1
View File
@@ -398,7 +398,7 @@ class Thread {
if (frame.params === null) {
continue;
}
if (frame.params.hasOwnProperty(paramName)) {
if (Object.prototype.hasOwnProperty.call(frame.params, paramName)) {
return frame.params[paramName];
}
return null;
+3 -3
View File
@@ -99,8 +99,8 @@ class ExtensionManager {
this.workerURLs = [];
/**
* Map of loaded extension URLs/IDs to service names.
* @type {Map.<string, string>}
* Map of loaded extension URLs/IDs (equivalent for built-in extensions) to service name.
* @type {Map.<string,string>}
* @private
*/
this._loadedExtensions = new Map();
@@ -596,7 +596,7 @@ class ExtensionManager {
getExtensionURLs () {
const extensionURLs = {};
for (const [extensionId, serviceName] of this._loadedExtensions.entries()) {
if (this.builtinExtensions.hasOwnProperty(extensionId)) {
if (Object.prototype.hasOwnProperty.call(this.builtinExtensions, extensionId)) {
continue;
}
+1 -1
View File
@@ -368,7 +368,7 @@ class Scratch3MakeyMakeyBlocks {
*/
addSequence (sequenceString, sequenceArray) {
// If we already have this sequence string, return.
if (this.sequences.hasOwnProperty(sequenceString)) {
if (Object.prototype.hasOwnProperty.call(this.sequences, sequenceString)) {
return;
}
this.sequences[sequenceString] = {
+2 -2
View File
@@ -15,7 +15,7 @@ const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYA
/**
* Enum for micro:bit BLE command protocol.
* https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md
* https://github.com/scratchfoundation/scratch-microbit-firmware/blob/master/protocol.md
* @readonly
* @enum {number}
*/
@@ -46,7 +46,7 @@ const BLEDataStoppedError = 'micro:bit extension stopped receiving data';
/**
* Enum for micro:bit protocol.
* https://github.com/LLK/scratch-microbit-firmware/blob/master/protocol.md
* https://github.com/scratchfoundation/scratch-microbit-firmware/blob/master/protocol.md
* @readonly
* @enum {string}
*/
+1 -1
View File
@@ -585,7 +585,7 @@ class Scratch3PenBlocks {
penState.color = (hsv.h / 360) * 100;
penState.saturation = hsv.s * 100;
penState.brightness = hsv.v * 100;
if (rgb.hasOwnProperty('a')) {
if (Object.prototype.hasOwnProperty.call(rgb, 'a')) {
penState.transparency = 100 * (1 - (rgb.a / 255.0));
} else {
penState.transparency = 0;
+2 -2
View File
@@ -7,7 +7,7 @@ const Cast = require('../../util/cast');
const MathUtil = require('../../util/math-util');
const Clone = require('../../util/clone');
const log = require('../../util/log');
const fetchWithTimeout = require('../../util/fetch-with-timeout');
const {fetchWithTimeout} = require('../../util/fetch-with-timeout');
/**
* Icon svg to be displayed in the blocks category menu, encoded as a data URI.
@@ -198,7 +198,7 @@ class Scratch3Text2SpeechBlocks {
*
* SCRATCH LOCALE
* Set by the editor, and used to store the language state in the project.
* Listed in l10n: https://github.com/LLK/scratch-l10n/blob/master/src/supported-locales.js
* Listed in l10n: https://github.com/scratchfoundation/scratch-l10n/blob/master/src/supported-locales.js
* SUPPORTED LOCALE
* A Scratch locale that has a corresponding extension locale.
* EXTENSION LOCALE
+3 -3
View File
@@ -2,7 +2,7 @@ const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');
const fetchWithTimeout = require('../../util/fetch-with-timeout');
const {fetchWithTimeout} = require('../../util/fetch-with-timeout');
const languageNames = require('scratch-translate-extension-languages');
const formatMessage = require('format-message');
@@ -222,11 +222,11 @@ class Scratch3TranslateBlocks {
getLanguageCodeFromArg (arg) {
const languageArg = Cast.toString(arg).toLowerCase();
// Check if the arg matches a language code in the menu.
if (languageNames.menuMap.hasOwnProperty(languageArg)) {
if (Object.prototype.hasOwnProperty.call(languageNames.menuMap, languageArg)) {
return languageArg;
}
// Check for a dropped-in language name, and convert to a language code.
if (languageNames.nameMap.hasOwnProperty(languageArg)) {
if (Object.prototype.hasOwnProperty.call(languageNames.nameMap, languageArg)) {
return languageNames.nameMap[languageArg];
}
@@ -167,7 +167,6 @@ class Scratch3VideoSensingBlocks {
if (stage) {
stage.videoTransparency = transparency;
}
return transparency;
}
/**
@@ -191,7 +190,6 @@ class Scratch3VideoSensingBlocks {
if (stage) {
stage.videoState = state;
}
return state;
}
/**
@@ -231,7 +229,8 @@ class Scratch3VideoSensingBlocks {
* @private
*/
_loop () {
setTimeout(this._loop.bind(this), Math.max(this.runtime.currentStepTime, Scratch3VideoSensingBlocks.INTERVAL));
const loopTime = Math.max(this.runtime.currentStepTime, Scratch3VideoSensingBlocks.INTERVAL);
this._loopInterval = setTimeout(this._loop.bind(this), loopTime);
// Add frame to detector
const time = Date.now();
@@ -251,6 +250,13 @@ class Scratch3VideoSensingBlocks {
}
}
/**
* Stop the video sampling loop. Only used for testing.
*/
_stopLoop () {
clearTimeout(this._loopInterval);
}
/**
* Create data for a menu in scratch-blocks format, consisting of an array
* of objects with text and value properties. The text is a translated
+11 -5
View File
@@ -23,7 +23,7 @@ const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
// scratch-svg-renderer fixes syntax that causes loading issues,
// and if optVersion is 2, fixes "quirks" associated with Scratch 2 SVGs,
const fixedSvgString = serializeSvgToString(loadSvgString(svgString, true /* fromVersion2 */));
// If the string changed, put back into storage
if (svgString !== fixedSvgString) {
svgString = fixedSvgString;
@@ -175,9 +175,13 @@ const readImage = new AsyncLimiter(_persistentReadImage, 25);
*/
const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
if (!costume || !costume.asset) { // TODO: We can probably remove this check...
// TODO: reject with an Error (breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('Costume load failed. Assets were missing.');
}
if (!runtime.v2BitmapAdapter) {
// TODO: reject with an Error (breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('No V2 Bitmap adapter present.');
}
@@ -277,6 +281,8 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) {
// somewhere and act on that error (like logging).
//
// Return a rejection to stop executing updateCostumeAsset.
// TODO: reject with an Error (breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('No V2 Bitmap adapter present.');
}
@@ -348,14 +354,14 @@ const handleCostumeLoadError = function (costume, runtime) {
const AssetType = runtime.storage.AssetType;
const isVector = costume.dataFormat === AssetType.ImageVector.runtimeFormat;
// Use default asset if original fails to load
costume.assetId = isVector ?
runtime.storage.defaultAssetId.ImageVector :
runtime.storage.defaultAssetId.ImageBitmap;
costume.asset = runtime.storage.get(costume.assetId);
costume.md5 = `${costume.assetId}.${costume.asset.dataFormat}`;
const defaultCostumePromise = (isVector) ?
loadVector_(costume, runtime) : loadBitmap_(costume, runtime);
@@ -367,7 +373,7 @@ const handleCostumeLoadError = function (costume, runtime) {
// Should be null if we got here because the costume was missing
loadedCostume.broken.asset = oldAsset;
loadedCostume.broken.dataFormat = oldDataFormat;
loadedCostume.broken.rotationCenterX = oldRotationX;
loadedCostume.broken.rotationCenterY = oldRotationY;
loadedCostume.broken.bitmapResolution = oldBitmapResolution;
@@ -409,7 +415,7 @@ const loadCostumeFromAsset = function (costume, runtime, optVersion) {
.catch(error => {
log.warn(`Error loading vector image: ${error}`);
return handleCostumeLoadError(costume, runtime);
});
}
return loadBitmap_(costume, runtime, rotationCenter, optVersion)
+1 -1
View File
@@ -48,7 +48,7 @@ class Mouse {
const drawableID = this.runtime.renderer.pick(x, y);
for (let i = 0; i < this.runtime.targets.length; i++) {
const target = this.runtime.targets[i];
if (target.hasOwnProperty('drawableID') &&
if (Object.prototype.hasOwnProperty.call(target, 'drawableID') &&
target.drawableID === drawableID) {
return target;
}
+30 -28
View File
@@ -331,7 +331,7 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
let target = null;
// List blocks don't come in with their target name set.
// Find the target by searching for a target with matching variable name/type.
if (!object.hasOwnProperty('target')) {
if (!Object.prototype.hasOwnProperty.call(object, 'target')) {
for (let i = 0; i < targets.length; i++) {
const currTarget = targets[i];
const listVariables = Object.keys(currTarget.variables).filter(key => {
@@ -369,7 +369,7 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
block.id = getVariableId(object.param, Variable.SCALAR_TYPE);
} else if (object.cmd === 'contentsOfList:') {
block.id = getVariableId(object.param, Variable.LIST_TYPE);
} else if (runtime.monitorBlockInfo.hasOwnProperty(block.opcode)) {
} else if (Object.prototype.hasOwnProperty.call(runtime.monitorBlockInfo, block.opcode)) {
block.id = runtime.monitorBlockInfo[block.opcode].getId(target.id, block.fields);
} else {
// If the opcode can't be found in the runtime monitorBlockInfo,
@@ -448,7 +448,7 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
* objects.
*/
const parseScratchAssets = function (object, runtime, topLevel, zip) {
if (!object.hasOwnProperty('objName')) {
if (!Object.prototype.hasOwnProperty.call(object, 'objName')) {
// Skip parsing monitors. Or any other objects missing objName.
return null;
}
@@ -462,7 +462,7 @@ const parseScratchAssets = function (object, runtime, topLevel, zip) {
// Costumes from JSON.
const costumePromises = assets.costumePromises;
if (object.hasOwnProperty('costumes')) {
if (Object.prototype.hasOwnProperty.call(object, 'costumes')) {
for (let i = 0; i < object.costumes.length; i++) {
const costumeSource = object.costumes[i];
const bitmapResolution = costumeSource.bitmapResolution || 1;
@@ -507,7 +507,7 @@ const parseScratchAssets = function (object, runtime, topLevel, zip) {
}
// Sounds from JSON
const {soundBank, soundPromises} = assets;
if (object.hasOwnProperty('sounds')) {
if (Object.prototype.hasOwnProperty.call(object, 'sounds')) {
for (let s = 0; s < object.sounds.length; s++) {
const soundSource = object.sounds[s];
const sound = {
@@ -566,8 +566,8 @@ const parseScratchAssets = function (object, runtime, topLevel, zip) {
* @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects.
*/
const parseScratchObject = function (object, runtime, extensions, topLevel, zip, assets) {
if (!object.hasOwnProperty('objName')) {
if (object.hasOwnProperty('listName')) {
if (!Object.prototype.hasOwnProperty.call(object, 'objName')) {
if (Object.prototype.hasOwnProperty.call(object, 'listName')) {
// Shim these objects so they can be processed as monitors
object.cmd = 'contentsOfList:';
object.param = object.listName;
@@ -583,10 +583,10 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
const sprite = new Sprite(blocks, runtime);
// Sprite/stage name from JSON.
if (object.hasOwnProperty('objName')) {
if (Object.prototype.hasOwnProperty.call(object, 'objName')) {
if (topLevel && object.objName !== 'Stage') {
for (const child of object.children) {
if (!child.hasOwnProperty('objName') && child.target === object.objName) {
if (!Object.prototype.hasOwnProperty.call(child, 'objName') && child.target === object.objName) {
child.target = 'Stage';
}
}
@@ -609,7 +609,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
const addBroadcastMsg = globalBroadcastMsgObj.broadcastMsgMapUpdater;
// Load target properties from JSON.
if (object.hasOwnProperty('variables')) {
if (Object.prototype.hasOwnProperty.call(object, 'variables')) {
for (let j = 0; j < object.variables.length; j++) {
const variable = object.variables[j];
// A variable is a cloud variable if:
@@ -632,7 +632,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
// If included, parse any and all comments on the object (this includes top-level
// workspace comments as well as comments attached to specific blocks)
const blockComments = {};
if (object.hasOwnProperty('scriptComments')) {
if (Object.prototype.hasOwnProperty.call(object, 'scriptComments')) {
const comments = object.scriptComments.map(commentDesc => {
const [
commentX,
@@ -667,7 +667,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
newComment.blockId = flattenedBlockIndex;
// Add this comment to the block comments object with its script index
// as the key
if (blockComments.hasOwnProperty(flattenedBlockIndex)) {
if (Object.prototype.hasOwnProperty.call(blockComments, flattenedBlockIndex)) {
blockComments[flattenedBlockIndex].push(newComment);
} else {
blockComments[flattenedBlockIndex] = [newComment];
@@ -684,7 +684,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
}
// If included, parse any and all scripts/blocks on the object.
if (object.hasOwnProperty('scripts')) {
if (Object.prototype.hasOwnProperty.call(object, 'scripts')) {
parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions, blockComments);
}
@@ -707,7 +707,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
// Update stage specific blocks (e.g. sprite clicked <=> stage clicked)
blocks.updateTargetSpecificBlocks(topLevel); // topLevel = isStage
if (object.hasOwnProperty('lists')) {
if (Object.prototype.hasOwnProperty.call(object, 'lists')) {
for (let k = 0; k < object.lists.length; k++) {
const list = object.lists[k];
const newVariable = new Variable(
@@ -720,32 +720,34 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
target.variables[newVariable.id] = newVariable;
}
}
if (object.hasOwnProperty('scratchX')) {
if (Object.prototype.hasOwnProperty.call(object, 'scratchX')) {
target.x = object.scratchX;
}
if (object.hasOwnProperty('scratchY')) {
if (Object.prototype.hasOwnProperty.call(object, 'scratchY')) {
target.y = object.scratchY;
}
if (object.hasOwnProperty('direction')) {
target.direction = object.direction;
if (Object.prototype.hasOwnProperty.call(object, 'direction')) {
// Sometimes the direction can be outside of the range: LLK/scratch-gui#5806
// wrapClamp it (like we do on RenderedTarget.setDirection)
target.direction = MathUtil.wrapClamp(object.direction, -179, 180);
}
if (object.hasOwnProperty('isDraggable')) {
if (Object.prototype.hasOwnProperty.call(object, 'isDraggable')) {
target.draggable = object.isDraggable;
}
if (object.hasOwnProperty('scale')) {
if (Object.prototype.hasOwnProperty.call(object, 'scale')) {
// SB2 stores as 1.0 = 100%; we use % in the VM.
target.size = object.scale * 100;
}
if (object.hasOwnProperty('visible')) {
if (Object.prototype.hasOwnProperty.call(object, 'visible')) {
target.visible = object.visible;
}
if (object.hasOwnProperty('currentCostumeIndex')) {
if (Object.prototype.hasOwnProperty.call(object, 'currentCostumeIndex')) {
// Current costume index can sometimes be a floating
// point number, use Math.floor to come up with an appropriate index
// and clamp it to the actual number of costumes the object has for good measure.
target.currentCostume = MathUtil.clamp(Math.floor(object.currentCostumeIndex), 0, object.costumes.length - 1);
}
if (object.hasOwnProperty('rotationStyle')) {
if (Object.prototype.hasOwnProperty.call(object, 'rotationStyle')) {
if (object.rotationStyle === 'none') {
target.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE;
} else if (object.rotationStyle === 'leftRight') {
@@ -754,16 +756,16 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
target.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND;
}
}
if (object.hasOwnProperty('tempoBPM')) {
if (Object.prototype.hasOwnProperty.call(object, 'tempoBPM')) {
target.tempo = object.tempoBPM;
}
if (object.hasOwnProperty('videoAlpha')) {
if (Object.prototype.hasOwnProperty.call(object, 'videoAlpha')) {
// SB2 stores alpha as opacity, where 1.0 is opaque.
// We convert to a percentage, and invert it so 100% is full transparency.
target.videoTransparency = 100 - (100 * object.videoAlpha);
}
if (object.hasOwnProperty('info')) {
if (object.info.hasOwnProperty('videoOn')) {
if (Object.prototype.hasOwnProperty.call(object, 'info')) {
if (Object.prototype.hasOwnProperty.call(object.info, 'videoOn')) {
if (object.info.videoOn) {
target.videoState = RenderedTarget.VIDEO_STATE.ON;
} else {
@@ -771,7 +773,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip,
}
}
}
if (object.hasOwnProperty('indexInLibrary')) {
if (Object.prototype.hasOwnProperty.call(object, 'indexInLibrary')) {
// Temporarily store the 'indexInLibrary' property from the sb2 file
// so that we can correctly order sprites in the target pane.
// This will be deleted after we are done parsing and ordering the targets list.
+3 -3
View File
@@ -9,13 +9,13 @@
* Keep this up-to-date as 3.0 blocks are renamed, changed, etc.
* Originally this was generated largely by a hand-guided scripting process.
* The relevant data lives here:
* https://github.com/LLK/scratch-flash/blob/master/src/Specs.as
* https://github.com/scratchfoundation/scratch-flash/blob/master/src/Specs.as
* (for the old opcode and argument order).
* and here:
* https://github.com/LLK/scratch-blocks/tree/develop/blocks_vertical
* https://github.com/scratchfoundation/scratch-blocks/tree/develop/blocks_vertical
* (for the new opcodes and argument names).
* and here:
* https://github.com/LLK/scratch-blocks/blob/develop/tests/
* https://github.com/scratchfoundation/scratch-blocks/blob/develop/tests/
* (for the shadow blocks created for each block).
* I started with the `commands` array in Specs.as, and discarded irrelevant
* properties. By hand, I matched the opcode name to the 3.0 opcode.
+50 -40
View File
@@ -173,7 +173,7 @@ const serializeFields = function (fields) {
for (const fieldName in fields) {
if (!hasOwnProperty.call(fields, fieldName)) continue;
obj[fieldName] = [fields[fieldName].value];
if (fields[fieldName].hasOwnProperty('id')) {
if (Object.prototype.hasOwnProperty.call(fields[fieldName], 'id')) {
obj[fieldName].push(fields[fieldName].id);
}
}
@@ -334,7 +334,7 @@ const serializeBlocks = function (blocks) {
const obj = Object.create(null);
const extensionIDs = new Set();
for (const blockID in blocks) {
if (!blocks.hasOwnProperty(blockID)) continue;
if (!Object.prototype.hasOwnProperty.call(blocks, blockID)) continue;
obj[blockID] = serializeBlock(blocks[blockID], blocks);
const extensionID = getExtensionIdForOpcode(blocks[blockID].opcode);
if (extensionID) {
@@ -436,16 +436,16 @@ const serializeCostume = function (costume) {
obj.bitmapResolution = costumeToSerialize.bitmapResolution;
obj.dataFormat = costumeToSerialize.dataFormat.toLowerCase();
obj.assetId = costumeToSerialize.assetId;
// serialize this property with the name 'md5ext' because that's
// what it's actually referring to. TODO runtime objects need to be
// updated to actually refer to this as 'md5ext' instead of 'md5'
// but that change should be made carefully since it is very
// pervasive
obj.md5ext = costumeToSerialize.md5;
obj.rotationCenterX = costumeToSerialize.rotationCenterX;
obj.rotationCenterY = costumeToSerialize.rotationCenterY;
@@ -460,7 +460,7 @@ const serializeCostume = function (costume) {
const serializeSound = function (sound) {
const obj = Object.create(null);
obj.name = sound.name;
const soundToSerialize = sound.broken || sound;
obj.assetId = soundToSerialize.assetId;
@@ -545,7 +545,7 @@ const serializeVariables = function (variables) {
const serializeComments = function (comments) {
const obj = Object.create(null);
for (const commentId in comments) {
if (!comments.hasOwnProperty(commentId)) continue;
if (!Object.prototype.hasOwnProperty.call(comments, commentId)) continue;
const comment = comments[commentId];
const serializedComment = Object.create(null);
@@ -590,13 +590,21 @@ const serializeTarget = function (target, extensions) {
obj.currentCostume = target.currentCostume;
obj.costumes = target.costumes.map(serializeCostume);
obj.sounds = target.sounds.map(serializeSound);
if (target.hasOwnProperty('volume')) obj.volume = target.volume;
if (target.hasOwnProperty('layerOrder')) obj.layerOrder = target.layerOrder;
if (Object.prototype.hasOwnProperty.call(target, 'volume')) obj.volume = target.volume;
if (Object.prototype.hasOwnProperty.call(target, 'layerOrder')) obj.layerOrder = target.layerOrder;
if (obj.isStage) { // Only the stage should have these properties
if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo;
if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency;
if (target.hasOwnProperty('videoState')) obj.videoState = target.videoState;
if (target.hasOwnProperty('textToSpeechLanguage')) obj.textToSpeechLanguage = target.textToSpeechLanguage;
if (Object.prototype.hasOwnProperty.call(target, 'tempo')) {
obj.tempo = target.tempo;
}
if (Object.prototype.hasOwnProperty.call(target, 'videoTransparency')) {
obj.videoTransparency = target.videoTransparency;
}
if (Object.prototype.hasOwnProperty.call(target, 'videoState')) {
obj.videoState = target.videoState;
}
if (Object.prototype.hasOwnProperty.call(target, 'textToSpeechLanguage')) {
obj.textToSpeechLanguage = target.textToSpeechLanguage;
}
} else { // The stage does not need the following properties, but sprites should
obj.visible = target.visible;
obj.x = target.x;
@@ -1017,7 +1025,7 @@ const deserializeBlocks = function (blocks) {
* SoundBank for the sound assets. null for unsupported objects.
*/
const parseScratchAssets = function (object, runtime, zip) {
if (!object.hasOwnProperty('name')) {
if (!Object.prototype.hasOwnProperty.call(object, 'name')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
return Promise.resolve(null);
@@ -1047,7 +1055,7 @@ const parseScratchAssets = function (object, runtime, zip) {
costumeSource.dataFormat ||
(costumeSource.assetType && costumeSource.assetType.runtimeFormat) || // older format
'png'; // if all else fails, guess that it might be a PNG
const costumeMd5Ext = costumeSource.hasOwnProperty('md5ext') ?
const costumeMd5Ext = Object.prototype.hasOwnProperty.call(costumeSource, 'md5ext') ?
costumeSource.md5ext : `${costumeSource.assetId}.${dataFormat}`;
costume.md5 = costumeMd5Ext;
costume.dataFormat = dataFormat;
@@ -1101,7 +1109,7 @@ const parseScratchAssets = function (object, runtime, zip) {
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
*/
const parseScratchObject = function (object, runtime, extensions, zip, assets) {
if (!object.hasOwnProperty('name')) {
if (!Object.prototype.hasOwnProperty.call(object, 'name')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
return Promise.resolve(null);
@@ -1113,14 +1121,14 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
const sprite = new Sprite(blocks, runtime);
// Sprite/stage name from JSON.
if (object.hasOwnProperty('name')) {
if (Object.prototype.hasOwnProperty.call(object, 'name')) {
sprite.name = object.name;
}
if (object.hasOwnProperty('blocks')) {
if (Object.prototype.hasOwnProperty.call(object, 'blocks')) {
deserializeBlocks(object.blocks);
// Take a second pass to create objects and add extensions
for (const blockId in object.blocks) {
if (!object.blocks.hasOwnProperty(blockId)) continue;
if (!Object.prototype.hasOwnProperty.call(object.blocks, blockId)) continue;
const blockJSON = object.blocks[blockId];
blocks.createBlock(blockJSON);
@@ -1138,22 +1146,22 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
// Create the first clone, and load its run-state from JSON.
const target = sprite.createClone(object.isStage ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER);
// Load target properties from JSON.
if (object.hasOwnProperty('tempo')) {
if (Object.prototype.hasOwnProperty.call(object, 'tempo')) {
target.tempo = object.tempo;
}
if (object.hasOwnProperty('volume')) {
if (Object.prototype.hasOwnProperty.call(object, 'volume')) {
target.volume = object.volume;
}
if (object.hasOwnProperty('videoTransparency')) {
if (Object.prototype.hasOwnProperty.call(object, 'videoTransparency')) {
target.videoTransparency = object.videoTransparency;
}
if (object.hasOwnProperty('videoState')) {
if (Object.prototype.hasOwnProperty.call(object, 'videoState')) {
target.videoState = object.videoState;
}
if (object.hasOwnProperty('textToSpeechLanguage')) {
if (Object.prototype.hasOwnProperty.call(object, 'textToSpeechLanguage')) {
target.textToSpeechLanguage = object.textToSpeechLanguage;
}
if (object.hasOwnProperty('variables')) {
if (Object.prototype.hasOwnProperty.call(object, 'variables')) {
for (const varId in object.variables) {
const variable = object.variables[varId];
// A variable is a cloud variable if:
@@ -1173,7 +1181,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
target.variables[newVariable.id] = newVariable;
}
}
if (object.hasOwnProperty('lists')) {
if (Object.prototype.hasOwnProperty.call(object, 'lists')) {
for (const listId in object.lists) {
const list = object.lists[listId];
const newList = new Variable(
@@ -1186,7 +1194,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
target.variables[newList.id] = newList;
}
}
if (object.hasOwnProperty('broadcasts')) {
if (Object.prototype.hasOwnProperty.call(object, 'broadcasts')) {
for (const broadcastId in object.broadcasts) {
const broadcast = object.broadcasts[broadcastId];
const newBroadcast = new Variable(
@@ -1200,7 +1208,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
target.variables[newBroadcast.id] = newBroadcast;
}
}
if (object.hasOwnProperty('comments')) {
if (Object.prototype.hasOwnProperty.call(object, 'comments')) {
for (const commentId in object.comments) {
const comment = object.comments[commentId];
const newComment = new Comment(
@@ -1218,37 +1226,39 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
target.comments[newComment.id] = newComment;
}
}
if (object.hasOwnProperty('x')) {
if (Object.prototype.hasOwnProperty.call(object, 'x')) {
target.x = object.x;
}
if (object.hasOwnProperty('y')) {
if (Object.prototype.hasOwnProperty.call(object, 'y')) {
target.y = object.y;
}
if (object.hasOwnProperty('direction')) {
target.direction = object.direction;
if (Object.prototype.hasOwnProperty.call(object, 'direction')) {
// Sometimes the direction can be outside of the range: LLK/scratch-gui#5806
// wrapClamp it (like we do on RenderedTarget.setDirection)
target.direction = MathUtil.wrapClamp(object.direction, -179, 180);
}
if (object.hasOwnProperty('size')) {
if (Object.prototype.hasOwnProperty.call(object, 'size')) {
target.size = object.size;
}
if (object.hasOwnProperty('visible')) {
if (Object.prototype.hasOwnProperty.call(object, 'visible')) {
target.visible = object.visible;
}
if (object.hasOwnProperty('currentCostume')) {
if (Object.prototype.hasOwnProperty.call(object, 'currentCostume')) {
target.currentCostume = MathUtil.clamp(object.currentCostume, 0, object.costumes.length - 1);
}
if (object.hasOwnProperty('rotationStyle')) {
if (Object.prototype.hasOwnProperty.call(object, 'rotationStyle')) {
target.rotationStyle = object.rotationStyle;
}
if (object.hasOwnProperty('isStage')) {
if (Object.prototype.hasOwnProperty.call(object, 'isStage')) {
target.isStage = object.isStage;
}
if (object.hasOwnProperty('targetPaneOrder')) {
if (Object.prototype.hasOwnProperty.call(object, 'targetPaneOrder')) {
// Temporarily store the 'targetPaneOrder' property
// so that we can correctly order sprites in the target pane.
// This will be deleted after we are done parsing and ordering the targets list.
target.targetPaneOrder = object.targetPaneOrder;
}
if (object.hasOwnProperty('draggable')) {
if (Object.prototype.hasOwnProperty.call(object, 'draggable')) {
target.draggable = object.draggable;
}
Promise.all(costumePromises).then(costumes => {
@@ -1288,7 +1298,7 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions)
// This will be undefined for extension blocks
const monitorBlockInfo = runtime.monitorBlockInfo[monitorData.opcode];
// Due to a bug (see https://github.com/LLK/scratch-vm/pull/2322), renamed list monitors may have been serialized
// Due to a bug (see https://github.com/scratchfoundation/scratch-vm/pull/2322), renamed list monitors may have been serialized
// with an outdated/incorrect LIST parameter. Fix it up to use the current name of the actual corresponding list.
if (monitorData.opcode === 'data_listcontents') {
const listTarget = monitorData.targetId ?
+12 -12
View File
@@ -401,7 +401,7 @@ class RenderedTarget extends Target {
* @param {!number} value Numerical magnitude of effect.
*/
setEffect (effectName, value) { // used by compiler
if (!this.effects.hasOwnProperty(effectName)) return;
if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) return;
this.effects[effectName] = value;
if (this.renderer) {
this.renderer.updateDrawableEffect(this.drawableID, effectName, value);
@@ -417,12 +417,12 @@ class RenderedTarget extends Target {
*/
clearEffects () { // used by compiler
for (const effectName in this.effects) {
if (!this.effects.hasOwnProperty(effectName)) continue;
if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) continue;
this.effects[effectName] = 0;
}
if (this.renderer) {
for (const effectName in this.effects) {
if (!this.effects.hasOwnProperty(effectName)) continue;
if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) continue;
this.renderer.updateDrawableEffect(this.drawableID, effectName, 0);
}
if (this.visible) {
@@ -691,7 +691,7 @@ class RenderedTarget extends Target {
this.renderer.updateDrawableSkinId(this.drawableID, costume.skinId);
for (const effectName in this.effects) {
if (!this.effects.hasOwnProperty(effectName)) continue;
if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) continue;
this.renderer.updateDrawableEffect(this.drawableID, effectName, this.effects[effectName]);
}
@@ -1029,25 +1029,25 @@ class RenderedTarget extends Target {
* @param {object} data An object with sprite info data to set.
*/
postSpriteInfo (data) {
const force = data.hasOwnProperty('force') ? data.force : null;
const isXChanged = data.hasOwnProperty('x');
const isYChanged = data.hasOwnProperty('y');
const force = Object.prototype.hasOwnProperty.call(data, 'force') ? data.force : null;
const isXChanged = Object.prototype.hasOwnProperty.call(data, 'x');
const isYChanged = Object.prototype.hasOwnProperty.call(data, 'y');
if (isXChanged || isYChanged) {
this.setXY(isXChanged ? data.x : this.x, isYChanged ? data.y : this.y, force);
}
if (data.hasOwnProperty('direction')) {
if (Object.prototype.hasOwnProperty.call(data, 'direction')) {
this.setDirection(data.direction);
}
if (data.hasOwnProperty('draggable')) {
if (Object.prototype.hasOwnProperty.call(data, 'draggable')) {
this.setDraggable(data.draggable);
}
if (data.hasOwnProperty('rotationStyle')) {
if (Object.prototype.hasOwnProperty.call(data, 'rotationStyle')) {
this.setRotationStyle(data.rotationStyle);
}
if (data.hasOwnProperty('visible')) {
if (Object.prototype.hasOwnProperty.call(data, 'visible')) {
this.setVisible(data.visible);
}
if (data.hasOwnProperty('size')) {
if (Object.prototype.hasOwnProperty.call(data, 'size')) {
this.setSize(data.size);
}
}
+27 -2
View File
@@ -1,3 +1,25 @@
/**
* @callback FetchFunction
* @param {RequestInfo|URL} input
* @param {RequestInit|undefined} [init]
* @returns {Promise<Response>}
*/
/**
* @type {FetchFunction}
*/
let myFetch = global.fetch;
/**
* Tell `fetchWithTimeout` to use a specific `fetch` function.
* By default, `fetchWithTimeout` will use the global `fetch` function.
* If there is no global `fetch`, then `fetchWithTimeout` will fail unless provided with an alternative.
* @param {FetchFunction} newFetch The new `fetch` function to use within fetchWithTimeout.
*/
const setFetch = newFetch => {
myFetch = newFetch;
};
/**
* Fetch a remote resource like `fetch` does, but with a time limit.
* @param {Request|string} resource Remote resource to fetch.
@@ -12,7 +34,7 @@ const fetchWithTimeout = (resource, init, timeout) => {
const signal = controller ? controller.signal : null;
// The fetch call races a timer.
return Promise.race([
fetch(resource, Object.assign({signal}, init)).then(response => {
myFetch(resource, Object.assign({signal}, init)).then(response => {
clearTimeout(timeoutID);
return response;
}, error => {
@@ -28,4 +50,7 @@ const fetchWithTimeout = (resource, init, timeout) => {
]);
};
module.exports = fetchWithTimeout;
module.exports = {
fetchWithTimeout,
setFetch
};
+2 -2
View File
@@ -62,7 +62,7 @@ class JSONRPC {
if (json.jsonrpc !== '2.0') {
throw new Error(`Bad or missing JSON-RPC version in message: ${json}`);
}
if (json.hasOwnProperty('method')) {
if (Object.prototype.hasOwnProperty.call(json, 'method')) {
this._handleRequest(json);
} else {
this._handleResponse(json);
@@ -98,7 +98,7 @@ class JSONRPC {
_handleRequest (json) {
const {method, params, id} = json;
const rawResult = this.didReceiveCall(method, params);
if (id) {
if (id !== null && typeof id !== 'undefined') {
Promise.resolve(rawResult).then(
result => {
this._sendResponse(id, result);
+5 -3
View File
@@ -21,8 +21,10 @@ class TaskQueue {
this._maxTokens = maxTokens;
this._refillRate = refillRate;
this._pendingTaskRecords = [];
this._tokenCount = options.hasOwnProperty('startingTokens') ? options.startingTokens : maxTokens;
this._maxTotalCost = options.hasOwnProperty('maxTotalCost') ? options.maxTotalCost : Infinity;
this._tokenCount = Object.prototype.hasOwnProperty.call(options, 'startingTokens') ?
options.startingTokens : maxTokens;
this._maxTotalCost = Object.prototype.hasOwnProperty.call(options, 'maxTotalCost') ?
options.maxTotalCost : Infinity;
this._timer = new Timer();
this._timer.start();
this._timeout = null;
@@ -53,7 +55,7 @@ class TaskQueue {
if (this._maxTotalCost < Infinity) {
const currentTotalCost = this._pendingTaskRecords.reduce((t, r) => t + r.cost, 0);
if (currentTotalCost + cost > this._maxTotalCost) {
return Promise.reject('Maximum total cost exceeded');
return Promise.reject(new Error('Maximum total cost exceeded'));
}
}
const newRecord = {
+1 -12
View File
@@ -8,7 +8,7 @@
* ... pass some time ...
* var timeDifference = timer.timeElapsed();
* ---
* Or, you can use the `time` and `relativeTime`
* Or, you can use the `time` function
* to do some measurement yourself.
*/
@@ -68,17 +68,6 @@ class Timer {
return this.nowObj.now();
}
/**
* Returns a time accurate relative to other times produced by this function.
* If possible, will use sub-millisecond precision.
* If not, will use millisecond precision.
* Not guaranteed to produce the same absolute values per-system.
* @returns {number} ms-scale accurate time relative to other relative times.
*/
relativeTime () {
return this.nowObj.now();
}
/**
* Start a timer for measuring elapsed time,
* at the most accurate precision possible.
+35 -9
View File
@@ -2,7 +2,6 @@ let _TextEncoder;
if (typeof TextEncoder === 'undefined') {
_TextEncoder = require('text-encoding').TextEncoder;
} else {
/* global TextEncoder */
_TextEncoder = TextEncoder;
}
const EventEmitter = require('events');
@@ -230,11 +229,18 @@ class VirtualMachine extends EventEmitter {
}
/**
* tw: Stop running the VM
* Note: This only stops the loop. It will not stop any threads the next time the VM starts
* @deprecated Used by old versions of TurboWarp. Superceded by upstream's quit()
*/
stop () {
this.runtime.stop();
this.quit();
}
/**
* Quit the VM, clearing any handles which might keep the process alive.
* Do not use the runtime after calling this method. This method is meant for test shutdown.
*/
quit () {
this.runtime.quit();
}
/**
@@ -462,10 +468,10 @@ class VirtualMachine extends EventEmitter {
return validationPromise
.then(validatedInput => this.deserializeProject(validatedInput[0], validatedInput[1]))
.then(() => this.runtime.emitProjectLoaded())
.then(() => this.runtime.handleProjectLoaded())
.catch(error => {
// Intentionally rejecting here (want errors to be handled by caller)
if (error.hasOwnProperty('validationError')) {
if (Object.prototype.hasOwnProperty.call(error, 'validationError')) {
return Promise.reject(JSON.stringify(error));
}
return Promise.reject(error);
@@ -672,6 +678,8 @@ class VirtualMachine extends EventEmitter {
const sb3 = require('./serialization/sb3');
return sb3.deserialize(projectJSON, runtime, zip);
}
// TODO: reject with an Error (possible breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('Unable to verify Scratch Project version.');
};
return deserializePromise()
@@ -810,14 +818,18 @@ class VirtualMachine extends EventEmitter {
if (projectVersion === 3) {
return this._addSprite3(validatedInput[0], validatedInput[1]);
}
// TODO: reject with an Error (possible breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject(`${errorPrefix} Unable to verify sprite version.`);
})
.then(() => this.runtime.emitProjectChanged())
.catch(error => {
// Intentionally rejecting here (want errors to be handled by caller)
if (error.hasOwnProperty('validationError')) {
if (Object.prototype.hasOwnProperty.call(error, 'validationError')) {
return Promise.reject(JSON.stringify(error));
}
// TODO: reject with an Error (possible breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject(`${errorPrefix} ${error}`);
});
}
@@ -876,6 +888,8 @@ class VirtualMachine extends EventEmitter {
});
}
// If the target cannot be found by id, return a rejected promise
// TODO: reject with an Error (possible breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject();
}
@@ -890,6 +904,8 @@ class VirtualMachine extends EventEmitter {
* @returns {?Promise} - a promise that resolves when the costume has been added
*/
addCostumeFromLibrary (md5ext, costumeObject) {
// TODO: reject with an Error (possible breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
if (!this.editingTarget) return Promise.reject();
return this.addCostume(md5ext, costumeObject, this.editingTarget.id, 2 /* optVersion */);
}
@@ -1441,6 +1457,13 @@ class VirtualMachine extends EventEmitter {
}
}
/**
* Delete all of the flyout blocks.
*/
clearFlyoutBlocks () {
this.runtime.flyoutBlocks.deleteAllBlocks();
}
/**
* Set an editing target. An editor UI can use this function to switch
* between editing different targets, sprites, etc.
@@ -1580,7 +1603,7 @@ class VirtualMachine extends EventEmitter {
lazyTargetList = this.runtime.targets
.filter(
// Don't report clones.
target => !target.hasOwnProperty('isOriginal') || target.isOriginal
target => !Object.prototype.hasOwnProperty.call(target, 'isOriginal') || target.isOriginal
).map(
target => target.toJSON()
);
@@ -1664,7 +1687,10 @@ class VirtualMachine extends EventEmitter {
*/
getTargetIdForDrawableId (drawableId) {
const target = this.runtime.getTargetByDrawableId(drawableId);
if (target && target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) {
if (target &&
Object.prototype.hasOwnProperty.call(target, 'id') &&
Object.prototype.hasOwnProperty.call(target, 'isStage') &&
!target.isStage) {
return target.id;
}
return null;
BIN
View File
Binary file not shown.
+2
View File
@@ -6,6 +6,8 @@ const path = require('path');
const oldRequire = Module.prototype.require;
Module.prototype.require = function (target) {
if (target.indexOf('/') === -1) {
// we really do just want to forward the arguments here
// eslint-disable-next-line prefer-rest-params
return oldRequire.apply(this, arguments);
}
-9
View File
@@ -78,15 +78,6 @@ class MockTimer {
return this._mockTime;
}
/**
* Returns a time accurate relative to other times produced by this function.
* @returns {number} ms-scale accurate time relative to other relative times.
* @memberof MockTimer
*/
relativeTime () {
return this._mockTime;
}
/**
* Start a timer for measuring elapsed time.
* @memberof MockTimer
+1 -1
View File
@@ -27,8 +27,8 @@ test('default cat', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
vm.start();
@@ -33,8 +33,8 @@ test('importing sb2 project where block comment is converted to workspace commen
const invalidComments = targetComments.filter(comment => typeof comment.blockId === 'number');
t.equal(invalidComments.length, 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
@@ -38,8 +38,8 @@ test('importing sb2 project where block comment is converted to workspace commen
const targetBlocks = Object.values(target.blocks._blocks);
t.equal(targetBlocks.length, 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
@@ -57,13 +57,15 @@ test('importing sb2 project with special chars in message names', t => {
t.equal(allBroadcastFields[ltPerfectMessageId].length, 1);
t.equal(allBroadcastFields[abMessageId].length, 1);
const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]);
const catMessageBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('BROADCAST_OPTION'));
const catMessageBlocks = catBlocks.filter(
block => Object.prototype.hasOwnProperty.call(block.fields, 'BROADCAST_OPTION')
);
t.equal(catMessageBlocks.length, 2);
t.equal(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId);
t.equal(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
@@ -57,13 +57,15 @@ test('importing sb3 project with special chars in message names', t => {
t.equal(allBroadcastFields[ltPerfectMessageId].length, 1);
t.equal(allBroadcastFields[abMessageId].length, 1);
const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]);
const catMessageBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('BROADCAST_OPTION'));
const catMessageBlocks = catBlocks.filter(
block => Object.prototype.hasOwnProperty.call(block.fields, 'BROADCAST_OPTION')
);
t.equal(catMessageBlocks.length, 2);
t.equal(catMessageBlocks[0].fields.BROADCAST_OPTION.id, ltPerfectMessageId);
t.equal(catMessageBlocks[1].fields.BROADCAST_OPTION.id, abMessageId);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+8 -12
View File
@@ -17,12 +17,6 @@ test('clone-cleanup', t => {
*/
let testStep = -1;
/**
* We test using setInterval; track the interval ID here so we can cancel it.
* @type {object}
*/
let testInterval = null;
const verifyCounts = (expectedClones, extraThreads) => {
// stage plus one sprite, plus clones
t.strictEqual(vm.runtime.targets.length, 2 + expectedClones,
@@ -60,17 +54,19 @@ test('clone-cleanup', t => {
break;
case 3:
// The second batch of clones has been created and the main thread has ended
verifyCounts(10, 0);
// The second batch of clones has been created and the main thread is about to end
verifyCounts(10, 1);
// After the main thread ends, do one last test step
setTimeout(() => testNextStep(), 1000);
break;
case 4:
// The second batch of clones has deleted themselves; everything is finished
verifyCounts(0, 0);
clearInterval(testInterval);
vm.quit();
t.end();
process.nextTick(process.exit);
break;
}
};
@@ -88,8 +84,8 @@ test('clone-cleanup', t => {
vm.greenFlag();
// Every second, advance the testing step
testInterval = setInterval(testNextStep, 1000);
// Let the project control the pace of the tests
vm.runtime.on('SAY', () => testNextStep());
});
});
+5 -2
View File
@@ -37,6 +37,7 @@ test('importing an sb2 project with cloud variables', t => {
// when the message is being sent to the server rather than on the client
t.equal(variable.isCloud, true);
vm.quit();
t.end();
});
});
@@ -60,6 +61,7 @@ test('importing an sb2 project with cloud variables at the limit for a project',
// All of the 8 stage variables should be cloud variables
t.equal(stageVars.filter(v => v.isCloud).length, 10);
vm.quit();
t.end();
});
});
@@ -85,6 +87,7 @@ test('importing an sb2 project with cloud variables exceeding the limit for a pr
// Only 8 of the variables should have the isCloud flag set to true
t.equal(stageVars.filter(v => v.isCloud).length, 10);
vm.quit();
t.end();
});
});
@@ -115,6 +118,7 @@ test('importing one project after the other resets cloud variable limit', t => {
t.equal(vm.runtime.canAddCloudVariable(), true);
vm.quit();
t.end();
});
});
@@ -145,8 +149,7 @@ test('local cloud variables get imported as regular variables', t => {
t.equal(spriteVars.length, 1);
t.equal(spriteVars[0].isCloud, false);
vm.quit();
t.end();
process.nextTick(process.exit); // This is needed because this is the end of the last test in this file!!!
});
});
+5 -2
View File
@@ -35,6 +35,7 @@ test('importing an sb3 project with cloud variables', t => {
t.equal(Number(variable.value), 100);
t.equal(variable.isCloud, true);
vm.quit();
t.end();
});
});
@@ -58,6 +59,7 @@ test('importing an sb3 project with cloud variables at the limit for a project',
// All of the 10 stage variables should be cloud variables
t.equal(stageVars.filter(v => v.isCloud).length, 10);
vm.quit();
t.end();
});
});
@@ -83,6 +85,7 @@ test('importing an sb3 project with cloud variables exceeding the limit for a pr
// Only 8 of the variables should have the isCloud flag set to true
t.equal(stageVars.filter(v => v.isCloud).length, 10);
vm.quit();
t.end();
});
});
@@ -111,6 +114,7 @@ test('importing one project after the other resets cloud variable limit', t => {
t.equal(vm.runtime.canAddCloudVariable(), true);
vm.quit();
t.end();
});
});
@@ -141,8 +145,7 @@ test('local cloud variables get imported as regular variables', t => {
t.equal(spriteVars.length, 1);
t.equal(spriteVars[0].isCloud, false);
vm.quit();
t.end();
process.nextTick(process.exit); // This is needed because this is the end of the last test in this file!!!
});
});
+1 -1
View File
@@ -70,8 +70,8 @@ test('importing sb2 project with comments', t => {
t.equal(stopAllBlock.comment, blockComments[4].id);
t.equal(stopAllBlock.opcode, 'control_stop');
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -70,8 +70,8 @@ test('load an sb3 project with comments', t => {
t.equal(stopAllBlock.comment, blockComments[4].id);
t.equal(stopAllBlock.opcode, 'control_stop');
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -19,8 +19,8 @@ test('complex', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Manipulate each target
+1 -1
View File
@@ -15,8 +15,8 @@ test('control', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length > 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -14,8 +14,8 @@ test('data', t => {
// Evaluate playground data and exit
vm.on('playgroundData', () => {
// @todo Additional tests
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
@@ -27,8 +27,8 @@ test('default cat', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
vm.start();
+1 -1
View File
@@ -15,8 +15,8 @@ test('event', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length > 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+12 -12
View File
@@ -27,7 +27,7 @@ const VirtualMachine = require('../../src/index');
* been reached.
*/
const whenThreadsComplete = (t, vm, timeLimit = 2000) => (
const whenThreadsComplete = (t, vm, uri, timeLimit = 5000) =>
// When the number of threads reaches 0 the test is expected to be complete.
new Promise((resolve, reject) => {
const intervalId = setInterval(() => {
@@ -44,6 +44,7 @@ const whenThreadsComplete = (t, vm, timeLimit = 2000) => (
}, 50);
const timeoutId = setTimeout(() => {
t.fail(`Timeout waiting for threads to complete: ${uri}`);
reject(new Error('time limit reached'));
}, timeLimit);
@@ -53,19 +54,23 @@ const whenThreadsComplete = (t, vm, timeLimit = 2000) => (
clearInterval(intervalId);
clearTimeout(timeoutId);
});
})
);
});
const executeDir = path.resolve(__dirname, '../fixtures/execute');
// Find files which end in ".sb", ".sb2", or ".sb3"
const fileFilter = /\.sb[23]?$/i;
fs.readdirSync(executeDir)
.filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3'))
.filter(uri => fileFilter.test(uri))
.forEach(uri => {
const run = (t, enableCompiler) => {
// Disable logging during this test.
log.suggest.deny('vm', 'error');
t.tearDown(() => log.suggest.clear());
const vm = new VirtualMachine();
// Map string messages to tap reporting methods. This will be used
// with events from scratch's runtime emitted on block instructions.
let didPlan;
@@ -86,6 +91,7 @@ fs.readdirSync(executeDir)
},
end () {
didEnd = true;
vm.quit();
t.end();
}
};
@@ -100,7 +106,6 @@ fs.readdirSync(executeDir)
return reporters.comment(text);
};
const vm = new VirtualMachine();
vm.attachStorage(makeTestStorage());
// Start the VM and initialize some vm properties.
@@ -118,12 +123,6 @@ fs.readdirSync(executeDir)
});
}
// Stop the runtime interval once the test is complete so the test
// process may naturally exit.
t.tearDown(() => {
vm.stop();
});
// Report the text of SAY events as testing instructions.
vm.runtime.on('SAY', (target, type, text) => reportVmResult(text));
@@ -133,7 +132,7 @@ fs.readdirSync(executeDir)
// the scratch project sent us a "end" message.
return vm.loadProject(project)
.then(() => vm.greenFlag())
.then(() => whenThreadsComplete(t, vm))
.then(() => whenThreadsComplete(t, vm, uri))
.then(() => {
// Setting a plan is not required but is a good idea.
if (!didPlan) {
@@ -146,6 +145,7 @@ fs.readdirSync(executeDir)
// it can be resolved.
if (!didEnd) {
t.fail('did not say "end"');
vm.quit();
t.end();
}
});
+1 -1
View File
@@ -34,8 +34,8 @@ compilerAndInterpreter('complex', (t, co) => {
const results = vm.runtime.targets[0].variables[resultKey].value;
t.deepEqual(results, ['3', '2', '1', 'stage']);
vm.quit();
t.end();
vm.stop();
});
// Start VM, load project, and run
+1 -1
View File
@@ -15,8 +15,8 @@ test('default', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -15,8 +15,8 @@ test('default', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -31,8 +31,8 @@ test('importing sb3 project with incorrect list monitor name', t => {
t.equal(monitorBlock.fields.LIST.value, renamedListName);
}
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+16 -1
View File
@@ -3,9 +3,19 @@ const tap = require('tap');
const {test} = tap;
const fs = require('fs');
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const dispatch = require('../../src/dispatch/central-dispatch');
const VirtualMachine = require('../../src/index');
tap.tearDown(() => process.nextTick(process.exit));
/**
* Call _stopLoop() on the Video Sensing extension.
* @param {VirtualMachine} vm - a VM instance which has loaded the 'videoSensing' extension.
*/
const stopVideoLoop = vm => {
// TODO: provide a general way to tell extensions to shut down
// Ideally we'd just dispose of the extension's Worker...
const serviceName = vm.extensionManager._loadedExtensions.get('videoSensing');
dispatch.call(serviceName, '_stopLoop');
};
test('Load external extensions', async t => {
const vm = new VirtualMachine();
@@ -25,6 +35,9 @@ test('Load external extensions', async t => {
});
});
}
stopVideoLoop(vm);
vm.quit();
t.end();
});
@@ -64,5 +77,7 @@ test('Load video sensing extension and video properties', async t => {
t.equal(vm.runtime.ioDevices.video._ghost, project.videoTransparency);
}
stopVideoLoop(vm);
vm.quit();
t.end();
});
@@ -17,8 +17,8 @@ test('sb2 project (originally from Scratch 1.4) with missing backdrop image shou
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
vm.start();
+1 -1
View File
@@ -15,8 +15,8 @@ test('looks', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -122,8 +122,8 @@ test('importing sb2 project with monitors', t => {
t.equal(monitorRecord.spriteName, null);
t.equal(monitorRecord.targetId, null);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
-1
View File
@@ -140,7 +140,6 @@ test('saving and loading sb2 project with monitors preserves sliderMin and slide
t.equal(monitorRecord.targetId, null);
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -247,8 +247,8 @@ test('importing sb3 project with monitors', t => {
t.equal(monitorRecord.targetId, null);
t.equal(vm.extensionManager.isExtensionLoaded('ev3'), true);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -15,8 +15,8 @@ test('motion', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length > 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -33,8 +33,8 @@ test('offline-custom-assets', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -37,8 +37,8 @@ test('pen', t => {
t.equal(originalPenState.penAttributes.diameter, 51);
t.equal(clonePenState.penAttributes.diameter, 42);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -15,8 +15,8 @@ test('procedure', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length === 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+73
View File
@@ -0,0 +1,73 @@
const Worker = require('tiny-worker');
const path = require('path');
const test = require('tap').test;
const VirtualMachine = require('../../src/index');
const dispatch = require('../../src/dispatch/central-dispatch');
const makeTestStorage = require('../fixtures/make-test-storage');
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
// it doesn't really matter which project we use: we're testing side effects of loading any project
const uri = path.resolve(__dirname, '../fixtures/default.sb3');
const project = readFileToBuffer(uri);
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
dispatch.workerClass = Worker;
test('runId', async t => {
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const isGuid = data => guidRegex.test(data);
const storage = makeTestStorage();
// add to this list every time the RunId should have changed
const runIdLog = [];
const pushRunId = () => {
const runId = storage.scratchFetch.getMetadata(storage.scratchFetch.RequestMetadata.RunId);
t.ok(isGuid(runId), 'Run IDs should always be a properly-formatted GUID', {runId});
runIdLog.push(runId);
};
const vm = new VirtualMachine();
vm.attachStorage(storage);
pushRunId(); // check that the initial run ID is valid
vm.start(); // starts the VM, not the project, so this doesn't change the run ID
vm.clear();
pushRunId(); // clearing the project conceptually changes the project identity does it DOES change the run ID
vm.setCompatibilityMode(false);
vm.setTurboMode(false);
await vm.loadProject(project);
pushRunId();
vm.greenFlag();
pushRunId();
// Turn the playgroundData event into a Promise that we can await
const playgroundDataPromise = new Promise(resolve => {
vm.on('playgroundData', data => resolve(data));
});
// Let the project run for a bit, then get playground data and stop the project
// This test doesn't need the playground data but it does need to run & stop the project
setTimeout(() => {
vm.getPlaygroundData();
vm.stopAll();
pushRunId();
}, 100);
// wait for the project to run to completion
await playgroundDataPromise;
for (let i = 0; i < runIdLog.length - 1; ++i) {
for (let j = i + 1; j < runIdLog.length; ++j) {
t.notSame(runIdLog[i], runIdLog[j], 'Run IDs should always be unique', {runIdLog});
}
}
vm.quit();
t.end();
});
@@ -19,8 +19,8 @@ test('Running project should not emit project changed event', t => {
// Evaluate playground data and exit
vm.on('playgroundData', () => {
t.equal(projectChanged, false);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -29,8 +29,8 @@ test('say/think and wait', t => {
// The test will fail if the project throws.
setTimeout(() => {
vm.stopAll();
vm.quit();
t.end();
process.nextTick(process.exit);
}, 2000);
});
});
@@ -25,8 +25,6 @@ const visibleTempoMonitorProjectUri = path.resolve(
__dirname, '../fixtures/visible-tempo-monitor-no-other-music-blocks.sb2');
const visibleTempoMonitorProject = readFileToBuffer(visibleTempoMonitorProjectUri);
tap.tearDown(() => process.nextTick(process.exit));
test('loading sb2 project with invisible video monitor should not load monitor or extension', t => {
const vm = new VirtualMachine();
vm.attachStorage(makeTestStorage());
@@ -39,6 +37,7 @@ test('loading sb2 project with invisible video monitor should not load monitor o
vm.loadProject(invisibleVideoMonitorProject).then(() => {
t.equal(vm.extensionManager.isExtensionLoaded('videoSensing'), false);
t.equal(vm.runtime._monitorState.size, 0);
vm.quit();
t.end();
});
});
@@ -55,6 +54,7 @@ test('loading sb2 project with visible video monitor should not load extension',
vm.loadProject(visibleVideoMonitorProject).then(() => {
t.equal(vm.extensionManager.isExtensionLoaded('videoSensing'), false);
t.equal(vm.runtime._monitorState.size, 0);
vm.quit();
t.end();
});
});
@@ -86,6 +86,7 @@ test('sb2 project with invisible music monitor should not load monitor or extens
vm.loadProject(invisibleTempoMonitorProject).then(() => {
t.equal(vm.extensionManager.isExtensionLoaded('music'), false);
t.equal(vm.runtime._monitorState.size, 0);
vm.quit();
t.end();
});
});
@@ -104,6 +105,7 @@ test('sb2 project with visible music monitor should load monitor and extension',
t.equal(vm.runtime._monitorState.size, 1);
t.equal(vm.runtime._monitorState.has('music_getTempo'), true);
t.equal(vm.runtime._monitorState.get('music_getTempo').visible, true);
vm.quit();
t.end();
});
});
+4 -5
View File
@@ -73,14 +73,14 @@ const test = tap.test;
test('load sb2 project with corrupted bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[1];
t.equal(greenGuySprite.getName(), 'GreenGuy');
t.equal(greenGuySprite.getCostumes().length, 1);
const corruptedCostume = greenGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'GreenGuy');
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
@@ -98,14 +98,14 @@ test('load and then save project with corrupted bitmap costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const greenGuySprite = resavedProject.targets[1];
t.equal(greenGuySprite.name, 'GreenGuy');
t.equal(greenGuySprite.costumes.length, 1);
const corruptedCostume = greenGuySprite.costumes[0];
t.equal(corruptedCostume.name, 'GreenGuy');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -124,5 +124,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -30,7 +30,7 @@ global.Image = function () {
width: 1,
height: 1
};
setTimeout(() => image.onload(), 1000);
return image;
};
@@ -73,14 +73,14 @@ const test = tap.test;
test('load sb2 project with corrupted vector costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[1];
t.equal(blueGuySprite.getName(), 'Blue Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const corruptedCostume = blueGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'Blue Guy 2');
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
@@ -98,14 +98,14 @@ test('load and then save project with corrupted vector costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const blueGuySprite = resavedProject.targets[1];
t.equal(blueGuySprite.name, 'Blue Guy');
t.equal(blueGuySprite.costumes.length, 1);
const corruptedCostume = blueGuySprite.costumes[0];
t.equal(corruptedCostume.name, 'Blue Guy 2');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -124,5 +124,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -58,14 +58,14 @@ const test = tap.test;
test('loading sb2 project with missing bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[1];
t.equal(greenGuySprite.getName(), 'GreenGuy');
t.equal(greenGuySprite.getCostumes().length, 1);
const missingCostume = greenGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'GreenGuy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -83,14 +83,14 @@ test('load and then save sb2 project with missing costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const greenGuySprite = resavedProject.targets[1];
t.equal(greenGuySprite.name, 'GreenGuy');
t.equal(greenGuySprite.costumes.length, 1);
const missingCostume = greenGuySprite.costumes[0];
t.equal(missingCostume.name, 'GreenGuy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -104,10 +104,9 @@ test('load and then save sb2 project with missing costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime);
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -55,14 +55,14 @@ const test = tap.test;
test('loading sb2 project with missing vector costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[1];
t.equal(blueGuySprite.getName(), 'Blue Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const missingCostume = blueGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'Blue Guy 2');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -80,14 +80,14 @@ test('load and then save sb2 project with missing costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const blueGuySprite = resavedProject.targets[1];
t.equal(blueGuySprite.name, 'Blue Guy');
t.equal(blueGuySprite.costumes.length, 1);
const missingCostume = blueGuySprite.costumes[0];
t.equal(missingCostume.name, 'Blue Guy 2');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -101,10 +101,9 @@ test('load and then save sb2 project with missing costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime);
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`);
t.end();
process.nextTick(process.exit);
});
+4 -5
View File
@@ -73,14 +73,14 @@ const test = tap.test;
test('load sb3 project with corrupted bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[1];
t.equal(greenGuySprite.getName(), 'Green Guy');
t.equal(greenGuySprite.getCostumes().length, 1);
const corruptedCostume = greenGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'Green Guy');
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
@@ -98,14 +98,14 @@ test('load and then save project with corrupted bitmap costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const greenGuySprite = resavedProject.targets[1];
t.equal(greenGuySprite.name, 'Green Guy');
t.equal(greenGuySprite.costumes.length, 1);
const corruptedCostume = greenGuySprite.costumes[0];
t.equal(corruptedCostume.name, 'Green Guy');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -124,5 +124,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -32,7 +32,7 @@ const FakeAudioEngine = function () {
if (soundDataString.includes('here is some')) {
return Promise.reject(new Error('mock audio engine broke'));
}
// Otherwise return fake data
return Promise.resolve({
id: fakeId++,
@@ -65,14 +65,14 @@ const test = tap.test;
test('load sb3 project with corrupted sound file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const catSprite = vm.runtime.targets[1];
t.equal(catSprite.getName(), 'Sprite1');
t.equal(catSprite.getSounds().length, 1);
const corruptedSound = catSprite.getSounds()[0];
t.equal(corruptedSound.name, 'Boop Sound Recording');
t.equal(corruptedSound.assetId, defaultSoundAssetId);
@@ -90,14 +90,14 @@ test('load and then save project with corrupted sound file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const catSprite = resavedProject.targets[1];
t.equal(catSprite.name, 'Sprite1');
t.equal(catSprite.sounds.length, 1);
const corruptedSound = catSprite.sounds[0];
t.equal(corruptedSound.name, 'Boop Sound Recording');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -116,5 +116,4 @@ test('serializeSounds saves orignal broken sound', t => {
t.equal(sound.fileName, `${brokenSoundMd5}.wav`);
t.equal(md5(sound.fileContent), brokenSoundMd5);
t.end();
process.nextTick(process.exit);
});
+4 -5
View File
@@ -52,14 +52,14 @@ const test = tap.test;
test('load sb3 project with corrupted vector costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[1];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const corruptedCostume = blueGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'costume1');
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
@@ -77,14 +77,14 @@ test('load and then save project with corrupted vector costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const blueGuySprite = resavedProject.targets[1];
t.equal(blueGuySprite.name, 'Blue Square Guy');
t.equal(blueGuySprite.costumes.length, 1);
const corruptedCostume = blueGuySprite.costumes[0];
t.equal(corruptedCostume.name, 'costume1');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -103,5 +103,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -58,14 +58,14 @@ const test = tap.test;
test('loading sb3 project with missing bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[1];
t.equal(greenGuySprite.getName(), 'Green Guy');
t.equal(greenGuySprite.getCostumes().length, 1);
const missingCostume = greenGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'Green Guy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -83,14 +83,14 @@ test('load and then save sb3 project with missing costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const greenGuySprite = resavedProject.targets[1];
t.equal(greenGuySprite.name, 'Green Guy');
t.equal(greenGuySprite.costumes.length, 1);
const missingCostume = greenGuySprite.costumes[0];
t.equal(missingCostume.name, 'Green Guy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -104,10 +104,9 @@ test('load and then save sb3 project with missing costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime);
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -32,13 +32,13 @@ const test = tap.test;
test('loading sb3 project with missing sound file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const catSprite = vm.runtime.targets[1];
t.equal(catSprite.getSounds().length, 1);
const missingSound = catSprite.getSounds()[0];
t.equal(missingSound.name, 'Boop Sound Recording');
// Sound should have original data but no asset
@@ -57,14 +57,14 @@ test('load and then save sb3 project with missing sound file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const catSprite = resavedProject.targets[1];
t.equal(catSprite.name, 'Sprite1');
t.equal(catSprite.sounds.length, 1);
const missingSound = catSprite.sounds[0];
t.equal(missingSound.name, 'Boop Sound Recording');
// Costume should have both default sound data (e.g. "Gray Question Sound" ^_^) and original data
@@ -78,10 +78,9 @@ test('load and then save sb3 project with missing sound file', t => {
test('serializeCostume does not save data for missing costume', t => {
const soundDescs = serializeSounds(vm.runtime);
t.equal(soundDescs.length, 1); // Should only have one sound, the pop sound for the stage
t.not(soundDescs[0].fileName, `${missingSoundAssetId}.wav`);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -35,14 +35,14 @@ const test = tap.test;
test('loading sb3 project with missing vector costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[1];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const missingCostume = blueGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -60,14 +60,14 @@ test('load and then save sb3 project with missing costume file', t => {
const resavedProject = JSON.parse(vm.toJSON());
t.equal(resavedProject.targets.length, 2);
const stage = resavedProject.targets[0];
t.ok(stage.isStage);
const blueGuySprite = resavedProject.targets[1];
t.equal(blueGuySprite.name, 'Blue Square Guy');
t.equal(blueGuySprite.costumes.length, 1);
const missingCostume = blueGuySprite.costumes[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -81,10 +81,9 @@ test('load and then save sb3 project with missing costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime);
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`);
t.end();
process.nextTick(process.exit);
});
+1 -1
View File
@@ -15,8 +15,8 @@ test('sensing', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length > 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+1 -1
View File
@@ -20,8 +20,8 @@ test('sound', t => {
vm.on('playgroundData', e => {
const threads = JSON.parse(e.threads);
t.ok(threads.length > 0);
vm.quit();
t.end();
process.nextTick(process.exit);
});
// Start VM, load project, and run
+3 -4
View File
@@ -78,14 +78,14 @@ const test = tap.test;
test('load sprite2 with corrupted bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[2];
t.equal(greenGuySprite.getName(), 'GreenGuy');
t.equal(greenGuySprite.getCostumes().length, 1);
const corruptedCostume = greenGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'GreenGuy');
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
@@ -104,7 +104,7 @@ test('load and then save sprite with corrupted costume file', t => {
t.equal(resavedSprite.name, 'GreenGuy');
t.equal(resavedSprite.costumes.length, 1);
const corruptedCostume = resavedSprite.costumes[0];
t.equal(corruptedCostume.name, 'GreenGuy');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -123,5 +123,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+3 -4
View File
@@ -77,14 +77,14 @@ const test = tap.test;
test('load sprite2 with corrupted vector costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[2];
t.equal(blueGuySprite.getName(), 'Blue Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const corruptedCostume = blueGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'Blue Guy 2');
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
@@ -103,7 +103,7 @@ test('load and then save sprite with corrupted costume file', t => {
t.equal(resavedSprite.name, 'Blue Guy');
t.equal(resavedSprite.costumes.length, 1);
const corruptedCostume = resavedSprite.costumes[0];
t.equal(corruptedCostume.name, 'Blue Guy 2');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -122,5 +122,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -61,14 +61,14 @@ const test = tap.test;
test('loading sprite2 with missing bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[2];
t.equal(greenGuySprite.getName(), 'GreenGuy');
t.equal(greenGuySprite.getCostumes().length, 1);
const missingCostume = greenGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'GreenGuy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -87,7 +87,7 @@ test('load and then save sprite2 with missing bitmap costume file', t => {
t.equal(resavedSprite.name, 'GreenGuy');
t.equal(resavedSprite.costumes.length, 1);
const missingCostume = resavedSprite.costumes[0];
t.equal(missingCostume.name, 'GreenGuy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -101,9 +101,8 @@ test('load and then save sprite2 with missing bitmap costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
t.equal(costumeDescs.length, 0);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -59,14 +59,14 @@ const test = tap.test;
test('loading sprite2 with missing vector costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[2];
t.equal(blueGuySprite.getName(), 'Blue Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const missingCostume = blueGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'Blue Guy 2');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -85,7 +85,7 @@ test('load and then save sprite2 with missing vector costume file', t => {
t.equal(resavedSprite.name, 'Blue Guy');
t.equal(resavedSprite.costumes.length, 1);
const missingCostume = resavedSprite.costumes[0];
t.equal(missingCostume.name, 'Blue Guy 2');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -99,9 +99,8 @@ test('load and then save sprite2 with missing vector costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
t.equal(costumeDescs.length, 0);
t.end();
process.nextTick(process.exit);
});
+3 -4
View File
@@ -78,14 +78,14 @@ const test = tap.test;
test('load sprite3 with corrupted bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[2];
t.equal(greenGuySprite.getName(), 'Green Guy');
t.equal(greenGuySprite.getCostumes().length, 1);
const corruptedCostume = greenGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'Green Guy');
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
@@ -104,7 +104,7 @@ test('load and then save sprite with corrupted costume file', t => {
t.equal(resavedSprite.name, 'Green Guy');
t.equal(resavedSprite.costumes.length, 1);
const corruptedCostume = resavedSprite.costumes[0];
t.equal(corruptedCostume.name, 'Green Guy');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -123,5 +123,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+3 -4
View File
@@ -57,14 +57,14 @@ const test = tap.test;
test('load sprite3 with corrupted vector costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[2];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const corruptedCostume = blueGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'costume1');
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
@@ -83,7 +83,7 @@ test('load and then save sprite with corrupted costume file', t => {
t.equal(resavedSprite.name, 'Blue Square Guy');
t.equal(resavedSprite.costumes.length, 1);
const corruptedCostume = resavedSprite.costumes[0];
t.equal(corruptedCostume.name, 'costume1');
// Resaved project costume should have the metadata that corresponds to the original broken costume
@@ -102,5 +102,4 @@ test('serializeCostume saves orignal broken costume', t => {
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -61,14 +61,14 @@ const test = tap.test;
test('loading sprite3 with missing bitmap costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const greenGuySprite = vm.runtime.targets[2];
t.equal(greenGuySprite.getName(), 'Green Guy');
t.equal(greenGuySprite.getCostumes().length, 1);
const missingCostume = greenGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'Green Guy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -87,7 +87,7 @@ test('load and then save sprite3 with missing bitmap costume file', t => {
t.equal(resavedSprite.name, 'Green Guy');
t.equal(resavedSprite.costumes.length, 1);
const missingCostume = resavedSprite.costumes[0];
t.equal(missingCostume.name, 'Green Guy');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -101,9 +101,8 @@ test('load and then save sprite3 with missing bitmap costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
t.equal(costumeDescs.length, 0);
t.end();
process.nextTick(process.exit);
});
+5 -6
View File
@@ -39,14 +39,14 @@ const test = tap.test;
test('loading sprite3 with missing vector costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[2];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const missingCostume = blueGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -65,7 +65,7 @@ test('load and then save sprite3 with missing vector costume file', t => {
t.equal(resavedSprite.name, 'Blue Square Guy');
t.equal(resavedSprite.costumes.length, 1);
const missingCostume = resavedSprite.costumes[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
@@ -79,9 +79,8 @@ test('load and then save sprite3 with missing vector costume file', t => {
test('serializeCostume does not save data for missing costume', t => {
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
t.equal(costumeDescs.length, 0);
t.end();
process.nextTick(process.exit);
});
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more