mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
Merge branch 'develop' of https://github.com/LLK/scratch-vm into bugfix/2089,2088,2087
This commit is contained in:
commit
19e6a1d4c9
18 changed files with 349 additions and 94 deletions
50
src/blocks/scratch3_core_example.js
Normal file
50
src/blocks/scratch3_core_example.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
const BlockType = require('../extension-support/block-type');
|
||||
|
||||
/**
|
||||
* An example core block implemented using the extension spec.
|
||||
* This is not loaded as part of the core blocks in the VM but it is provided
|
||||
* and used as part of tests.
|
||||
*/
|
||||
class Scratch3CoreExample {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} metadata for this extension and its blocks.
|
||||
*/
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'coreExample',
|
||||
name: 'CoreEx', // This string does not need to be translated as this extension is only used as an example.
|
||||
blocks: [
|
||||
{
|
||||
func: 'MAKE_A_VARIABLE',
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'make a variable (CoreEx)'
|
||||
},
|
||||
{
|
||||
opcode: 'exampleOpcode',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'example block'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Example opcode just returns the name of the stage target.
|
||||
* @returns {string} The name of the first target in the project.
|
||||
*/
|
||||
exampleOpcode () {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
return stage ? stage.getName() : 'no stage yet';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Scratch3CoreExample;
|
|
@ -35,6 +35,39 @@ class CentralDispatch extends SharedDispatch {
|
|||
this.workers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously call a particular method on a particular service provided locally.
|
||||
* Calling this function on a remote service will fail.
|
||||
* @param {string} service - the name of the service.
|
||||
* @param {string} method - the name of the method.
|
||||
* @param {*} [args] - the arguments to be copied to the method, if any.
|
||||
* @returns {*} - the return value of the service method.
|
||||
*/
|
||||
callSync (service, method, ...args) {
|
||||
const {provider, isRemote} = this._getServiceProvider(service);
|
||||
if (provider) {
|
||||
if (isRemote) {
|
||||
throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`);
|
||||
}
|
||||
|
||||
return provider[method].apply(provider, args);
|
||||
}
|
||||
throw new Error(`Provider not found for service: ${service}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously set a local object as the global provider of the specified service.
|
||||
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
||||
* @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'.
|
||||
* @param {object} provider - a local object which provides this service.
|
||||
*/
|
||||
setServiceSync (service, provider) {
|
||||
if (this.services.hasOwnProperty(service)) {
|
||||
log.warn(`Central dispatch replacing existing service provider for ${service}`);
|
||||
}
|
||||
this.services[service] = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a local object as the global provider of the specified service.
|
||||
* WARNING: Any method on the provider can be called from any worker within the dispatch system.
|
||||
|
@ -45,10 +78,7 @@ class CentralDispatch extends SharedDispatch {
|
|||
setService (service, provider) {
|
||||
/** Return a promise for consistency with {@link WorkerDispatch#setService} */
|
||||
try {
|
||||
if (this.services.hasOwnProperty(service)) {
|
||||
log.warn(`Central dispatch replacing existing service provider for ${service}`);
|
||||
}
|
||||
this.services[service] = provider;
|
||||
this.setServiceSync(service, provider);
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
|
|
|
@ -205,9 +205,18 @@ class BlockUtility {
|
|||
* @return {Array.<Thread>} List of threads started by this function.
|
||||
*/
|
||||
startHats (requestedHat, optMatchFields, optTarget) {
|
||||
return (
|
||||
this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget)
|
||||
);
|
||||
// Store thread and sequencer to ensure we can return to the calling block's context.
|
||||
// startHats may execute further blocks and dirty the BlockUtility's execution context
|
||||
// and confuse the calling block when we return to it.
|
||||
const callerThread = this.thread;
|
||||
const callerSequencer = this.sequencer;
|
||||
const result = this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget);
|
||||
|
||||
// Restore thread and sequencer to prior values before we return to the calling block.
|
||||
this.thread = callerThread;
|
||||
this.sequencer = callerSequencer;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ const Blocks = require('./blocks');
|
|||
const BlockType = require('../extension-support/block-type');
|
||||
const Profiler = require('./profiler');
|
||||
const Sequencer = require('./sequencer');
|
||||
const execute = require('./execute.js');
|
||||
const ScratchBlocksConstants = require('./scratch-blocks-constants');
|
||||
const TargetType = require('../extension-support/target-type');
|
||||
const Thread = require('./thread');
|
||||
|
@ -122,16 +123,6 @@ const cloudDataManager = () => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Predefined "Converted block info" for a separator between blocks in a block category
|
||||
* @type {ConvertedBlockInfo}
|
||||
*/
|
||||
const ConvertedSeparator = {
|
||||
info: {},
|
||||
json: null,
|
||||
xml: '<sep gap="36"/>'
|
||||
};
|
||||
|
||||
/**
|
||||
* Numeric ID for Runtime._step in Profiler instances.
|
||||
* @type {number}
|
||||
|
@ -316,7 +307,7 @@ class Runtime extends EventEmitter {
|
|||
// I/O related data.
|
||||
/** @type {Object.<string, Object>} */
|
||||
this.ioDevices = {
|
||||
clock: new Clock(),
|
||||
clock: new Clock(this),
|
||||
cloud: new Cloud(this),
|
||||
keyboard: new Keyboard(this),
|
||||
mouse: new Mouse(this),
|
||||
|
@ -865,14 +856,11 @@ class Runtime extends EventEmitter {
|
|||
}
|
||||
|
||||
for (const blockInfo of extensionInfo.blocks) {
|
||||
if (blockInfo === '---') {
|
||||
categoryInfo.blocks.push(ConvertedSeparator);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
||||
const opcode = convertedBlock.json.type;
|
||||
categoryInfo.blocks.push(convertedBlock);
|
||||
if (convertedBlock.json) {
|
||||
const opcode = convertedBlock.json.type;
|
||||
if (blockInfo.blockType !== BlockType.EVENT) {
|
||||
this._primitives[opcode] = convertedBlock.info.func;
|
||||
}
|
||||
|
@ -882,6 +870,7 @@ class Runtime extends EventEmitter {
|
|||
restartExistingThreads: blockInfo.shouldRestartExistingThreads
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Error parsing block: ', {block: blockInfo, error: e});
|
||||
}
|
||||
|
@ -985,6 +974,25 @@ class Runtime extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ExtensionBlockMetadata into data ready for scratch-blocks.
|
||||
* @param {ExtensionBlockMetadata} blockInfo - the block info to convert
|
||||
* @param {CategoryInfo} categoryInfo - the category for this block
|
||||
* @returns {ConvertedBlockInfo} - the converted & original block information
|
||||
* @private
|
||||
*/
|
||||
_convertForScratchBlocks (blockInfo, categoryInfo) {
|
||||
if (blockInfo === '---') {
|
||||
return this._convertSeparatorForScratchBlocks(blockInfo);
|
||||
}
|
||||
|
||||
if (blockInfo.blockType === BlockType.BUTTON) {
|
||||
return this._convertButtonForScratchBlocks(blockInfo);
|
||||
}
|
||||
|
||||
return this._convertBlockForScratchBlocks(blockInfo, categoryInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ExtensionBlockMetadata into scratch-blocks JSON & XML, and generate a proxy function.
|
||||
* @param {ExtensionBlockMetadata} blockInfo - the block to convert
|
||||
|
@ -992,7 +1000,7 @@ class Runtime extends EventEmitter {
|
|||
* @returns {ConvertedBlockInfo} - the converted & original block information
|
||||
* @private
|
||||
*/
|
||||
_convertForScratchBlocks (blockInfo, categoryInfo) {
|
||||
_convertBlockForScratchBlocks (blockInfo, categoryInfo) {
|
||||
const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`;
|
||||
|
||||
const blockJSON = {
|
||||
|
@ -1134,6 +1142,43 @@ class Runtime extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a separator between blocks categories or sub-categories.
|
||||
* @param {ExtensionBlockMetadata} blockInfo - the block to convert
|
||||
* @param {CategoryInfo} categoryInfo - the category for this block
|
||||
* @returns {ConvertedBlockInfo} - the converted & original block information
|
||||
* @private
|
||||
*/
|
||||
_convertSeparatorForScratchBlocks (blockInfo) {
|
||||
return {
|
||||
info: blockInfo,
|
||||
xml: '<sep gap="36"/>'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a button for scratch-blocks. A button has no opcode but specifies a callback name in the `func` field.
|
||||
* @param {ExtensionBlockMetadata} buttonInfo - the button to convert
|
||||
* @property {string} func - the callback name
|
||||
* @param {CategoryInfo} categoryInfo - the category for this button
|
||||
* @returns {ConvertedBlockInfo} - the converted & original button information
|
||||
* @private
|
||||
*/
|
||||
_convertButtonForScratchBlocks (buttonInfo) {
|
||||
// for now we only support these pre-defined callbacks handled in scratch-blocks
|
||||
const supportedCallbackKeys = ['MAKE_A_LIST', 'MAKE_A_PROCEDURE', 'MAKE_A_VARIABLE'];
|
||||
if (supportedCallbackKeys.indexOf(buttonInfo.func) < 0) {
|
||||
log.error(`Custom button callbacks not supported yet: ${buttonInfo.func}`);
|
||||
}
|
||||
|
||||
const extensionMessageContext = this.makeMessageContextForTarget();
|
||||
const buttonText = maybeFormatMessage(buttonInfo.text, extensionMessageContext);
|
||||
return {
|
||||
info: buttonInfo,
|
||||
xml: `<button text="${buttonText}" callbackKey="${buttonInfo.func}"></button>`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback
|
||||
* from string#replace. In addition to the return value the JSON and XML items in the context will be filled.
|
||||
|
@ -1631,6 +1676,12 @@ class Runtime extends EventEmitter {
|
|||
// Start the thread with this top block.
|
||||
newThreads.push(instance._pushThread(topBlockId, target));
|
||||
}, optTarget);
|
||||
// For compatibility with Scratch 2, edge triggered hats need to be processed before
|
||||
// threads are stepped. See ScratchRuntime.as for original implementation
|
||||
newThreads.forEach(thread => {
|
||||
execute(this.sequencer, thread);
|
||||
thread.goToNextBlock();
|
||||
});
|
||||
return newThreads;
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,6 @@ class Sequencer {
|
|||
activeThread.status === Thread.STATUS_DONE) {
|
||||
// Finished with this thread.
|
||||
stoppedThread = true;
|
||||
this.runtime.updateCurrentMSecs();
|
||||
}
|
||||
}
|
||||
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
|
||||
|
|
|
@ -8,6 +8,11 @@ const BlockType = {
|
|||
*/
|
||||
BOOLEAN: 'Boolean',
|
||||
|
||||
/**
|
||||
* A button (not an actual block) for some special action, like making a variable
|
||||
*/
|
||||
BUTTON: 'button',
|
||||
|
||||
/**
|
||||
* Command block
|
||||
*/
|
||||
|
|
|
@ -9,6 +9,10 @@ const BlockType = require('./block-type');
|
|||
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods
|
||||
|
||||
const builtinExtensions = {
|
||||
// This is an example that isn't loaded with the other core blocks,
|
||||
// but serves as a reference for loading core blocks as extensions.
|
||||
coreExample: () => require('../blocks/scratch3_core_example'),
|
||||
// These are the non-core built-in extensions.
|
||||
pen: () => require('../extensions/scratch3_pen'),
|
||||
wedo2: () => require('../extensions/scratch3_wedo2'),
|
||||
music: () => require('../extensions/scratch3_music'),
|
||||
|
@ -107,6 +111,30 @@ class ExtensionManager {
|
|||
return this._loadedExtensions.has(extensionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously load an internal extension (core or non-core) by ID. This call will
|
||||
* fail if the provided id is not does not match an internal extension.
|
||||
* @param {string} extensionId - the ID of an internal extension
|
||||
*/
|
||||
loadExtensionIdSync (extensionId) {
|
||||
if (!builtinExtensions.hasOwnProperty(extensionId)) {
|
||||
log.warn(`Could not find extension ${extensionId} in the built in extensions.`);
|
||||
return;
|
||||
}
|
||||
|
||||
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
|
||||
if (this.isExtensionLoaded(extensionId)) {
|
||||
const message = `Rejecting attempt to load a second extension with ID ${extensionId}`;
|
||||
log.warn(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = builtinExtensions[extensionId]();
|
||||
const extensionInstance = new extension(this.runtime);
|
||||
const serviceName = this._registerInternalExtension(extensionInstance);
|
||||
this._loadedExtensions.set(extensionId, serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an extension by URL or internal extension ID
|
||||
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
|
||||
|
@ -118,14 +146,14 @@ class ExtensionManager {
|
|||
if (this.isExtensionLoaded(extensionURL)) {
|
||||
const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
|
||||
log.warn(message);
|
||||
return Promise.reject(new Error(message));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const extension = builtinExtensions[extensionURL]();
|
||||
const extensionInstance = new extension(this.runtime);
|
||||
return this._registerInternalExtension(extensionInstance).then(serviceName => {
|
||||
const serviceName = this._registerInternalExtension(extensionInstance);
|
||||
this._loadedExtensions.set(extensionURL, serviceName);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -149,7 +177,7 @@ class ExtensionManager {
|
|||
dispatch.call('runtime', '_refreshExtensionPrimitives', info);
|
||||
})
|
||||
.catch(e => {
|
||||
log.error(`Failed to refresh buildtin extension primitives: ${JSON.stringify(e)}`);
|
||||
log.error(`Failed to refresh built-in extension primitives: ${JSON.stringify(e)}`);
|
||||
})
|
||||
);
|
||||
return Promise.all(allPromises);
|
||||
|
@ -162,6 +190,15 @@ class ExtensionManager {
|
|||
return [id, workerInfo.extensionURL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously collect extension metadata from the specified service and begin the extension registration process.
|
||||
* @param {string} serviceName - the name of the service hosting the extension.
|
||||
*/
|
||||
registerExtensionServiceSync (serviceName) {
|
||||
const info = dispatch.callSync(serviceName, 'getInfo');
|
||||
this._registerExtensionInfo(serviceName, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect extension metadata from the specified service and begin the extension registration process.
|
||||
* @param {string} serviceName - the name of the service hosting the extension.
|
||||
|
@ -190,17 +227,15 @@ class ExtensionManager {
|
|||
/**
|
||||
* Register an internal (non-Worker) extension object
|
||||
* @param {object} extensionObject - the extension object to register
|
||||
* @returns {Promise} resolved once the extension is fully registered or rejected on failure
|
||||
* @returns {string} The name of the registered extension service
|
||||
*/
|
||||
_registerInternalExtension (extensionObject) {
|
||||
const extensionInfo = extensionObject.getInfo();
|
||||
const fakeWorkerId = this.nextExtensionWorker++;
|
||||
const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`;
|
||||
return dispatch.setService(serviceName, extensionObject)
|
||||
.then(() => {
|
||||
dispatch.call('extensions', 'registerExtensionService', serviceName);
|
||||
dispatch.setServiceSync(serviceName, extensionObject);
|
||||
dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName);
|
||||
return serviceName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -341,16 +376,28 @@ class ExtensionManager {
|
|||
blockAllThreads: false,
|
||||
arguments: {}
|
||||
}, blockInfo);
|
||||
blockInfo.opcode = this._sanitizeID(blockInfo.opcode);
|
||||
blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode);
|
||||
blockInfo.text = blockInfo.text || blockInfo.opcode;
|
||||
|
||||
if (blockInfo.blockType !== BlockType.EVENT) {
|
||||
switch (blockInfo.blockType) {
|
||||
case BlockType.EVENT:
|
||||
if (blockInfo.func) {
|
||||
log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
|
||||
}
|
||||
break;
|
||||
case BlockType.BUTTON:
|
||||
if (blockInfo.opcode) {
|
||||
log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!blockInfo.opcode) {
|
||||
throw new Error('Missing opcode for block');
|
||||
}
|
||||
|
||||
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
||||
|
||||
/**
|
||||
* This is only here because the VM performs poorly when blocks return promises.
|
||||
* @TODO make it possible for the VM to resolve a promise and continue during the same Scratch "tick"
|
||||
*/
|
||||
// Avoid promise overhead if possible
|
||||
if (dispatch._isRemoteService(serviceName)) {
|
||||
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
|
||||
} else {
|
||||
|
@ -358,12 +405,11 @@ class ExtensionManager {
|
|||
const func = serviceObject[blockInfo.func];
|
||||
if (func) {
|
||||
blockInfo.func = func.bind(serviceObject);
|
||||
} else if (blockInfo.blockType !== BlockType.EVENT) {
|
||||
} else {
|
||||
throw new Error(`Could not find extension block function called ${blockInfo.func}`);
|
||||
}
|
||||
}
|
||||
} else if (blockInfo.func) {
|
||||
log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return blockInfo;
|
||||
|
|
|
@ -948,8 +948,9 @@ class Boost {
|
|||
const motor = this.motor(portID);
|
||||
if (motor) {
|
||||
motor._status = feedback;
|
||||
if (feedback === (BoostPortFeedback.COMPLETED ^ BoostPortFeedback.IDLE) &&
|
||||
motor.pendingPromiseFunction) {
|
||||
// Makes sure that commands resolve both when they actually complete and when they fail
|
||||
const commandCompleted = feedback & (BoostPortFeedback.COMPLETED ^ BoostPortFeedback.DISCARDED);
|
||||
if (commandCompleted && motor.pendingPromiseFunction) {
|
||||
motor.pendingPromiseFunction();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const Timer = require('../util/timer');
|
|||
|
||||
class Clock {
|
||||
constructor (runtime) {
|
||||
this._projectTimer = new Timer();
|
||||
this._projectTimer = new Timer({now: () => runtime.currentMSecs});
|
||||
this._projectTimer.start();
|
||||
this._pausedTime = null;
|
||||
this._paused = false;
|
||||
|
|
|
@ -151,6 +151,11 @@ class VirtualMachine extends EventEmitter {
|
|||
|
||||
this.extensionManager = new ExtensionManager(this.runtime);
|
||||
|
||||
// Load core extensions
|
||||
for (const id of CORE_EXTENSIONS) {
|
||||
this.extensionManager.loadExtensionIdSync(id);
|
||||
}
|
||||
|
||||
this.blockListener = this.blockListener.bind(this);
|
||||
this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
|
||||
this.monitorBlockListener = this.monitorBlockListener.bind(this);
|
||||
|
@ -493,14 +498,6 @@ class VirtualMachine extends EventEmitter {
|
|||
installTargets (targets, extensions, wholeProject) {
|
||||
const extensionPromises = [];
|
||||
|
||||
if (wholeProject) {
|
||||
CORE_EXTENSIONS.forEach(extensionID => {
|
||||
if (!this.extensionManager.isExtensionLoaded(extensionID)) {
|
||||
extensionPromises.push(this.extensionManager.loadExtensionURL(extensionID));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extensions.extensionIDs.forEach(extensionID => {
|
||||
if (!this.extensionManager.isExtensionLoaded(extensionID)) {
|
||||
const extensionURL = extensions.extensionURLs.get(extensionID) || extensionID;
|
||||
|
|
BIN
test/fixtures/edge-triggered-hat.sb3
vendored
BIN
test/fixtures/edge-triggered-hat.sb3
vendored
Binary file not shown.
BIN
test/fixtures/execute/hat-thread-execution.sb2
vendored
Normal file
BIN
test/fixtures/execute/hat-thread-execution.sb2
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
@ -7,7 +7,7 @@ const Thread = require('../../src/engine/thread');
|
|||
const Runtime = require('../../src/engine/runtime');
|
||||
const execute = require('../../src/engine/execute.js');
|
||||
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/loudness-hat-block.sb2');
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/timer-greater-than-hat.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const checkIsHatThread = (t, vm, hatThread) => {
|
||||
|
@ -24,8 +24,8 @@ const checkIsStackClickThread = (t, vm, stackClickThread) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* loudness-hat-block.sb2 contains a single stack
|
||||
* when loudness > 10
|
||||
* timer-greater-than-hat.sb2 contains a single stack
|
||||
* when timer > -1
|
||||
* change color effect by 25
|
||||
* The intention is to make sure that the hat block condition is evaluated
|
||||
* on each frame.
|
||||
|
@ -111,7 +111,7 @@ test('edge activated hat thread not added twice', t => {
|
|||
*/
|
||||
test('edge activated hat should trigger for both sprites when sprite is duplicated', t => {
|
||||
|
||||
// Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that
|
||||
// Project that is similar to timer-greater-than-hat.sb2, but has code on the sprite so that
|
||||
// the sprite can be duplicated
|
||||
const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3');
|
||||
const projectWithSprite = readFileToBuffer(projectWithSpriteUri);
|
||||
|
@ -134,9 +134,6 @@ test('edge activated hat should trigger for both sprites when sprite is duplicat
|
|||
t.equal(vm.runtime.threads.length, 1);
|
||||
checkIsHatThread(t, vm, vm.runtime.threads[0]);
|
||||
t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
|
||||
// Run execute on the thread to populate the runtime's
|
||||
// _edgeActivatedHatValues object
|
||||
execute(vm.runtime.sequencer, vm.runtime.threads[0]);
|
||||
let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
|
||||
val + Object.keys(target._edgeActivatedHatValues).length, 0);
|
||||
t.equal(numTargetEdgeHats, 1);
|
||||
|
@ -145,7 +142,6 @@ test('edge activated hat should trigger for both sprites when sprite is duplicat
|
|||
vm.runtime._step();
|
||||
// Check that the runtime's _edgeActivatedHatValues object has two separate keys
|
||||
// after execute is run on each thread
|
||||
vm.runtime.threads.forEach(thread => execute(vm.runtime.sequencer, thread));
|
||||
numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
|
||||
val + Object.keys(target._edgeActivatedHatValues).length, 0);
|
||||
t.equal(numTargetEdgeHats, 2);
|
||||
|
|
|
@ -4,6 +4,9 @@ const Worker = require('tiny-worker');
|
|||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
|
@ -52,7 +55,7 @@ test('internal extension', t => {
|
|||
t.ok(extension.status.constructorCalled);
|
||||
|
||||
t.notOk(extension.status.getInfoCalled);
|
||||
return vm.extensionManager._registerInternalExtension(extension).then(() => {
|
||||
vm.extensionManager._registerInternalExtension(extension);
|
||||
t.ok(extension.status.getInfoCalled);
|
||||
|
||||
const func = vm.runtime.getOpcodeFunction('testInternalExtension_go');
|
||||
|
@ -70,5 +73,36 @@ test('internal extension', t => {
|
|||
// Second menu is a dynamic menu and therefore should be a function.
|
||||
t.type(
|
||||
vm.runtime._blockInfo[0].menus[1].json.args0[0].options, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('load sync', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager.loadExtensionIdSync('coreExample');
|
||||
t.ok(vm.extensionManager.isExtensionLoaded('coreExample'));
|
||||
|
||||
t.equal(vm.runtime._blockInfo.length, 1);
|
||||
|
||||
// blocks should be an array of two items: a button pseudo-block and a reporter block.
|
||||
t.equal(vm.runtime._blockInfo[0].blocks.length, 2);
|
||||
t.type(vm.runtime._blockInfo[0].blocks[0].info, 'object');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[0].info.func, 'MAKE_A_VARIABLE');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[0].info.blockType, 'button');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[1].info, 'object');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.opcode, 'exampleOpcode');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.blockType, 'reporter');
|
||||
|
||||
// Test the opcode function
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'no stage yet');
|
||||
|
||||
const sprite = new Sprite(null, vm.runtime);
|
||||
sprite.name = 'Stage';
|
||||
const stage = new RenderedTarget(sprite, vm.runtime);
|
||||
stage.isStage = true;
|
||||
vm.runtime.targets = [stage];
|
||||
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'Stage');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
|
|
@ -62,3 +62,21 @@ test('remote', t => {
|
|||
.then(() => runServiceTest('RemoteDispatchTest', t), e => t.fail(e))
|
||||
.then(() => dispatch._remoteCall(worker, 'dispatch', 'terminate'), e => t.fail(e));
|
||||
});
|
||||
|
||||
test('local, sync', t => {
|
||||
dispatch.setServiceSync('SyncDispatchTest', new DispatchTestService());
|
||||
|
||||
const a = dispatch.callSync('SyncDispatchTest', 'returnFortyTwo');
|
||||
t.equal(a, 42);
|
||||
|
||||
const b = dispatch.callSync('SyncDispatchTest', 'doubleArgument', 9);
|
||||
t.equal(b, 18);
|
||||
|
||||
const c = dispatch.callSync('SyncDispatchTest', 'doubleArgument', 123);
|
||||
t.equal(c, 246);
|
||||
|
||||
t.throws(() => dispatch.callSync('SyncDispatchTest', 'throwException'),
|
||||
new Error('This is a test exception thrown by DispatchTest'));
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
|
|
@ -12,6 +12,11 @@ const testExtensionInfo = {
|
|||
id: 'test',
|
||||
name: 'fake test extension',
|
||||
blocks: [
|
||||
{
|
||||
func: 'MAKE_A_VARIABLE',
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'this is a button'
|
||||
},
|
||||
{
|
||||
opcode: 'reporter',
|
||||
blockType: BlockType.REPORTER,
|
||||
|
@ -58,6 +63,11 @@ const testExtensionInfo = {
|
|||
]
|
||||
};
|
||||
|
||||
const testButton = function (t, button) {
|
||||
t.same(button.json, null); // should be null or undefined
|
||||
t.equal(button.xml, '<button text="this is a button" callbackKey="MAKE_A_VARIABLE"></button>');
|
||||
};
|
||||
|
||||
const testReporter = function (t, reporter) {
|
||||
t.equal(reporter.json.type, 'test_reporter');
|
||||
t.equal(reporter.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND);
|
||||
|
@ -72,7 +82,7 @@ const testReporter = function (t, reporter) {
|
|||
};
|
||||
|
||||
const testSeparator = function (t, separator) {
|
||||
t.equal(separator.json, null);
|
||||
t.same(separator.json, null); // should be null or undefined
|
||||
t.equal(separator.xml, '<sep gap="36"/>');
|
||||
};
|
||||
|
||||
|
@ -153,9 +163,15 @@ test('registerExtensionPrimitives', t => {
|
|||
runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => {
|
||||
t.equal(blocksInfo.length, testExtensionInfo.blocks.length);
|
||||
|
||||
// Note that this also implicitly tests that block order is preserved
|
||||
const [reporter, separator, command, conditional, loop] = blocksInfo;
|
||||
blocksInfo.forEach(blockInfo => {
|
||||
// `true` here means "either an object or a non-empty string but definitely not null or undefined"
|
||||
t.true(blockInfo.info, 'Every block and pseudo-block must have a non-empty "info" field');
|
||||
});
|
||||
|
||||
// Note that this also implicitly tests that block order is preserved
|
||||
const [button, reporter, separator, command, conditional, loop] = blocksInfo;
|
||||
|
||||
testButton(t, button);
|
||||
testReporter(t, reporter);
|
||||
testSeparator(t, separator);
|
||||
testCommand(t, command);
|
||||
|
|
|
@ -23,12 +23,15 @@ test('cycle', t => {
|
|||
setTimeout(() => {
|
||||
c.resetProjectTimer();
|
||||
setTimeout(() => {
|
||||
t.ok(c.projectTimer() > 0);
|
||||
// The timer shouldn't advance until all threads have been stepped
|
||||
t.ok(c.projectTimer() === 0);
|
||||
c.pause();
|
||||
t.ok(c.projectTimer() > 0);
|
||||
t.ok(c.projectTimer() === 0);
|
||||
c.resume();
|
||||
t.ok(c.projectTimer() > 0);
|
||||
t.ok(c.projectTimer() === 0);
|
||||
t.end();
|
||||
}, 100);
|
||||
}, 100);
|
||||
rt._step();
|
||||
t.ok(c.projectTimer() > 0);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue