Initial commit of 001code-html Scratch frontend project.
Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
297
scratch-vm/test/unit/blocks_control.js
Normal file
297
scratch-vm/test/unit/blocks_control.js
Normal file
@@ -0,0 +1,297 @@
|
||||
const test = require('tap').test;
|
||||
const Control = require('../../src/blocks/scratch3_control');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const BlockUtility = require('../../src/engine/block-utility');
|
||||
|
||||
test('getPrimitives', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
t.type(c.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('repeat', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
let i = 0;
|
||||
const repeat = 10;
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
startBranch: function () {
|
||||
i++;
|
||||
c.repeat({TIMES: repeat}, util);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.repeat({TIMES: 10}, util);
|
||||
t.strictEqual(util.stackFrame.loopCounter, -1);
|
||||
t.strictEqual(i, repeat);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('repeat rounds with round()', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
const roundingTest = (inputForRepeat, expectedTimes) => {
|
||||
// Test harness (mocks `util`)
|
||||
let i = 0;
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
startBranch: function () {
|
||||
i++;
|
||||
c.repeat({TIMES: inputForRepeat}, util);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.repeat({TIMES: inputForRepeat}, util);
|
||||
t.strictEqual(i, expectedTimes);
|
||||
};
|
||||
|
||||
// Execute tests
|
||||
roundingTest(3.2, 3);
|
||||
roundingTest(3.7, 4);
|
||||
roundingTest(3.5, 4);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('repeatUntil', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
let i = 0;
|
||||
const repeat = 10;
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
startBranch: function () {
|
||||
i++;
|
||||
c.repeatUntil({CONDITION: (i === repeat)}, util);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.repeatUntil({CONDITION: (i === repeat)}, util);
|
||||
t.strictEqual(i, repeat);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('repeatWhile', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
let i = 0;
|
||||
const repeat = 10;
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
startBranch: function () {
|
||||
i++;
|
||||
// Note !== instead of ===
|
||||
c.repeatWhile({CONDITION: (i !== repeat)}, util);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.repeatWhile({CONDITION: (i !== repeat)}, util);
|
||||
t.strictEqual(i, repeat);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('forEach', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
const variableValues = [];
|
||||
const variable = {value: 0};
|
||||
let value;
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
target: {
|
||||
lookupOrCreateVariable: function () {
|
||||
return variable;
|
||||
}
|
||||
},
|
||||
startBranch: function () {
|
||||
variableValues.push(variable.value);
|
||||
c.forEach({VARIABLE: {}, VALUE: value}, util);
|
||||
}
|
||||
};
|
||||
|
||||
// for each (variable) in "5"
|
||||
// ..should yield variable values 1, 2, 3, 4, 5
|
||||
util.stackFrame = Object.create(null);
|
||||
variableValues.splice(0);
|
||||
variable.value = 0;
|
||||
value = '5';
|
||||
c.forEach({VARIABLE: {}, VALUE: value}, util);
|
||||
t.deepEqual(variableValues, [1, 2, 3, 4, 5]);
|
||||
|
||||
// for each (variable) in 4
|
||||
// ..should yield variable values 1, 2, 3, 4
|
||||
util.stackFrame = Object.create(null);
|
||||
variableValues.splice(0);
|
||||
variable.value = 0;
|
||||
value = 4;
|
||||
c.forEach({VARIABLE: {}, VALUE: value}, util);
|
||||
t.deepEqual(variableValues, [1, 2, 3, 4]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('forever', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
let i = 0;
|
||||
const util = {
|
||||
startBranch: function (branchNum, isLoop) {
|
||||
i++;
|
||||
t.strictEqual(branchNum, 1);
|
||||
t.strictEqual(isLoop, true);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.forever(null, util);
|
||||
t.strictEqual(i, 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('if / ifElse', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
let i = 0;
|
||||
const util = {
|
||||
startBranch: function (branchNum) {
|
||||
i += branchNum;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.if({CONDITION: true}, util);
|
||||
t.strictEqual(i, 1);
|
||||
c.if({CONDITION: false}, util);
|
||||
t.strictEqual(i, 1);
|
||||
c.ifElse({CONDITION: true}, util);
|
||||
t.strictEqual(i, 2);
|
||||
c.ifElse({CONDITION: false}, util);
|
||||
t.strictEqual(i, 4);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stop', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
const state = {
|
||||
stopAll: 0,
|
||||
stopOtherTargetThreads: 0,
|
||||
stopThisScript: 0
|
||||
};
|
||||
const util = {
|
||||
stopAll: function () {
|
||||
state.stopAll++;
|
||||
},
|
||||
stopOtherTargetThreads: function () {
|
||||
state.stopOtherTargetThreads++;
|
||||
},
|
||||
stopThisScript: function () {
|
||||
state.stopThisScript++;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.stop({STOP_OPTION: 'all'}, util);
|
||||
c.stop({STOP_OPTION: 'other scripts in sprite'}, util);
|
||||
c.stop({STOP_OPTION: 'other scripts in stage'}, util);
|
||||
c.stop({STOP_OPTION: 'this script'}, util);
|
||||
t.strictEqual(state.stopAll, 1);
|
||||
t.strictEqual(state.stopOtherTargetThreads, 2);
|
||||
t.strictEqual(state.stopThisScript, 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('counter, incrCounter, clearCounter', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Default value
|
||||
t.strictEqual(c.getCounter(), 0);
|
||||
|
||||
c.incrCounter();
|
||||
c.incrCounter();
|
||||
t.strictEqual(c.getCounter(), 2);
|
||||
|
||||
c.clearCounter();
|
||||
t.strictEqual(c.getCounter(), 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('allAtOnce', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
|
||||
// Test harness (mocks `util`)
|
||||
let ran = false;
|
||||
const util = {
|
||||
startBranch: function () {
|
||||
ran = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute test
|
||||
c.allAtOnce({}, util);
|
||||
t.true(ran);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('wait', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Control(rt);
|
||||
const args = {DURATION: .01};
|
||||
const waitTime = args.DURATION * 1000;
|
||||
const startTest = Date.now();
|
||||
const thresholdSmall = 1000 / 60; // only allow the wait to end one 60Hz frame early
|
||||
const thresholdLarge = 1000 / 3; // be less picky about when the wait ends, in case CPU load makes the VM run slowly
|
||||
let yields = 0;
|
||||
const util = new BlockUtility();
|
||||
const mockUtil = {
|
||||
stackFrame: {},
|
||||
yield: () => yields++,
|
||||
stackTimerNeedsInit: util.stackTimerNeedsInit,
|
||||
startStackTimer: util.startStackTimer,
|
||||
stackTimerFinished: util.stackTimerFinished
|
||||
};
|
||||
|
||||
c.wait(args, mockUtil);
|
||||
t.equal(yields, 1, 'First wait block yielded');
|
||||
|
||||
// Spin the cpu until enough time passes
|
||||
let timeElapsed = 0;
|
||||
while (timeElapsed < waitTime) {
|
||||
timeElapsed = mockUtil.stackFrame.timer.timeElapsed();
|
||||
// In case util.timer is broken - have our own "exit"
|
||||
if (Date.now() - startTest > timeElapsed + thresholdSmall) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
c.wait(args, mockUtil);
|
||||
t.equal(yields, 1, 'Second call after timeElapsed does not yield');
|
||||
t.equal(waitTime, mockUtil.stackFrame.duration);
|
||||
t.ok(timeElapsed >= (waitTime - thresholdSmall),
|
||||
`Wait block ended too early: ${timeElapsed} < ${waitTime} - ${thresholdSmall}`);
|
||||
t.ok(timeElapsed <= (waitTime + thresholdLarge),
|
||||
`Wait block ended too late: ${timeElapsed} > ${waitTime} + ${thresholdLarge}`);
|
||||
t.end();
|
||||
});
|
||||
49
scratch-vm/test/unit/blocks_data.js
Normal file
49
scratch-vm/test/unit/blocks_data.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const test = require('tap').test;
|
||||
const Data = require('../../src/blocks/scratch3_data');
|
||||
|
||||
const blocks = new Data();
|
||||
|
||||
const lists = {};
|
||||
const util = {
|
||||
target: {
|
||||
lookupOrCreateList (id, name) {
|
||||
if (!(name in lists)) {
|
||||
lists[name] = {value: []};
|
||||
}
|
||||
return lists[name];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test('getItemNumOfList returns the index of an item (basic)', t => {
|
||||
lists.list = {value: ['apple', 'taco', 'burrito', 'extravaganza']};
|
||||
const args = {ITEM: 'burrito', LIST: {name: 'list'}};
|
||||
const index = blocks.getItemNumOfList(args, util);
|
||||
t.strictEqual(index, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getItemNumOfList returns 0 when an item is not found', t => {
|
||||
lists.list = {value: ['aaaaapple', 'burrito']};
|
||||
const args = {ITEM: 'jump', LIST: {name: 'list'}};
|
||||
const index = blocks.getItemNumOfList(args, util);
|
||||
t.strictEqual(index, 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getItemNumOfList uses Scratch comparison', t => {
|
||||
lists.list = {value: ['jump', 'Jump', '123', 123, 800]};
|
||||
const args = {LIST: {name: 'list'}};
|
||||
|
||||
// Be case-insensitive:
|
||||
args.ITEM = 'Jump';
|
||||
t.strictEqual(blocks.getItemNumOfList(args, util), 1);
|
||||
|
||||
// Be type-insensitive:
|
||||
args.ITEM = 123;
|
||||
t.strictEqual(blocks.getItemNumOfList(args, util), 3);
|
||||
args.ITEM = '800';
|
||||
t.strictEqual(blocks.getItemNumOfList(args, util), 5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
114
scratch-vm/test/unit/blocks_data_infinity.js
Normal file
114
scratch-vm/test/unit/blocks_data_infinity.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const test = require('tap').test;
|
||||
const Data = require('../../src/blocks/scratch3_data');
|
||||
|
||||
const blocks = new Data();
|
||||
|
||||
const lists = {};
|
||||
const util = {
|
||||
target: {
|
||||
lookupOrCreateList (id, name) {
|
||||
if (!(name in lists)) {
|
||||
lists[name] = {value: []};
|
||||
}
|
||||
return lists[name];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test('List with postive infinity primitive contains postive infinity', t => {
|
||||
lists.list = {value: [Infinity]};
|
||||
let args = {ITEM: Infinity, LIST: {name: 'list'}};
|
||||
let contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '[Infinity] contains Infinity');
|
||||
|
||||
lists.list = {value: [Infinity]};
|
||||
args = {ITEM: 'Infinity', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '[Infinity] contains "Infinity"');
|
||||
|
||||
lists.list = {value: [Infinity]};
|
||||
args = {ITEM: 'INFINITY', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '[Infinity] contains "INFINITY"');
|
||||
|
||||
lists.list = {value: ['Infinity']};
|
||||
args = {ITEM: Infinity, LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["Infinity"] contains Infinity');
|
||||
|
||||
lists.list = {value: ['Infinity']};
|
||||
args = {ITEM: 'Infinity', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["Infinity"] contains "Infinity"');
|
||||
|
||||
lists.list = {value: ['Infinity']};
|
||||
args = {ITEM: 'INFINITY', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["Infinity"] contains "INFINITY"');
|
||||
|
||||
lists.list = {value: ['INFINITY']};
|
||||
args = {ITEM: Infinity, LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["INFINITY"] contains Infinity');
|
||||
|
||||
lists.list = {value: ['INFINITY']};
|
||||
args = {ITEM: 'Infinity', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["INFINITY"] contains "Infinity"');
|
||||
|
||||
lists.list = {value: ['INFINITY']};
|
||||
args = {ITEM: 'INFINITY', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["INFINITY"] contains "INFINITY"');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('List with negative infinity primitive contains negative infinity', t => {
|
||||
lists.list = {value: [-Infinity]};
|
||||
let args = {ITEM: -Infinity, LIST: {name: 'list'}};
|
||||
let contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '[-Infinity] contains -Infinity');
|
||||
|
||||
lists.list = {value: [-Infinity]};
|
||||
args = {ITEM: '-Infinity', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '[-Infinity] contains "-Infinity"');
|
||||
|
||||
lists.list = {value: [-Infinity]};
|
||||
args = {ITEM: '-INFINITY', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '[-Infinity] contains "-INFINITY"');
|
||||
|
||||
lists.list = {value: ['-Infinity']};
|
||||
args = {ITEM: -Infinity, LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["-Infinity"] contains -Infinity');
|
||||
|
||||
lists.list = {value: ['-Infinity']};
|
||||
args = {ITEM: '-Infinity', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["-Infinity"] contains "-Infinity"');
|
||||
|
||||
lists.list = {value: ['-Infinity']};
|
||||
args = {ITEM: '-INFINITY', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["-Infinity"] contains "-INFINITY"');
|
||||
|
||||
lists.list = {value: ['-INFINITY']};
|
||||
args = {ITEM: -Infinity, LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["-INFINITY"] contains -Infinity');
|
||||
|
||||
lists.list = {value: ['-INFINITY']};
|
||||
args = {ITEM: '-Infinity', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["-INFINITY"] contains "-Infinity"');
|
||||
|
||||
lists.list = {value: ['-INFINITY']};
|
||||
args = {ITEM: '-INFINITY', LIST: {name: 'list'}};
|
||||
contains = blocks.listContainsItem(args, util);
|
||||
t.strictEqual(contains, true, '["-INFINITY"] contains "-INFINITY"');
|
||||
|
||||
t.end();
|
||||
});
|
||||
116
scratch-vm/test/unit/blocks_event.js
Normal file
116
scratch-vm/test/unit/blocks_event.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const Blocks = require('../../src/engine/blocks');
|
||||
const BlockUtility = require('../../src/engine/block-utility');
|
||||
const Event = require('../../src/blocks/scratch3_event');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Target = require('../../src/engine/target');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
|
||||
test('#760 - broadcastAndWait', t => {
|
||||
const broadcastAndWaitBlock = {
|
||||
id: 'broadcastAndWaitBlock',
|
||||
fields: {
|
||||
BROADCAST_OPTION: {
|
||||
id: 'testBroadcastID',
|
||||
value: 'message'
|
||||
}
|
||||
},
|
||||
inputs: Object,
|
||||
block: 'fakeBlock',
|
||||
opcode: 'event_broadcastandwait',
|
||||
next: null,
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
const receiveMessageBlock = {
|
||||
id: 'receiveMessageBlock',
|
||||
fields: {
|
||||
BROADCAST_OPTION: {
|
||||
id: 'testBroadcastID',
|
||||
value: 'message'
|
||||
}
|
||||
},
|
||||
inputs: Object,
|
||||
block: 'fakeBlock',
|
||||
opcode: 'event_whenbroadcastreceived',
|
||||
next: null,
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
const rt = new Runtime();
|
||||
const e = new Event(rt);
|
||||
const b = new Blocks(rt);
|
||||
b.createBlock(broadcastAndWaitBlock);
|
||||
b.createBlock(receiveMessageBlock);
|
||||
const tgt = new Target(rt, b);
|
||||
tgt.isStage = true;
|
||||
tgt.createVariable('testBroadcastID', 'message', Variable.BROADCAST_MESSAGE_TYPE);
|
||||
|
||||
rt.addTarget(tgt);
|
||||
|
||||
let th = rt._pushThread('broadcastAndWaitBlock', t);
|
||||
const util = new BlockUtility();
|
||||
util.sequencer = rt.sequencer;
|
||||
util.thread = th;
|
||||
util.runtime = rt;
|
||||
|
||||
// creates threads
|
||||
e.broadcastAndWait({BROADCAST_OPTION: {id: 'testBroadcastID', name: 'message'}}, util);
|
||||
t.strictEqual(rt.threads.length, 2);
|
||||
t.strictEqual(rt.threads[1].topBlock, 'receiveMessageBlock');
|
||||
// yields when some thread is active
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: {id: 'testBroadcastID', name: 'message'}}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
// does not yield once all threads are done
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
rt.threads[1].status = Thread.STATUS_DONE;
|
||||
rt.threads.splice(1, 1);
|
||||
e.broadcastAndWait({BROADCAST_OPTION: {id: 'testBroadcastID', name: 'message'}}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_RUNNING);
|
||||
|
||||
// restarts done threads that are in runtime threads
|
||||
rt.updateThreadMap();
|
||||
th = rt._pushThread('broadcastAndWaitBlock', tgt);
|
||||
util.thread = th;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: {id: 'testBroadcastID', name: 'message'}}, util);
|
||||
t.strictEqual(rt.threads.length, 3);
|
||||
t.strictEqual(rt.threads[2].status, Thread.STATUS_RUNNING);
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
// yields when some restarted thread is active
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: {id: 'testBroadcastID', name: 'message'}}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
// does not yield once all threads are done
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
rt.threads[2].status = Thread.STATUS_DONE;
|
||||
rt.threads.splice(2, 1);
|
||||
e.broadcastAndWait({BROADCAST_OPTION: {id: 'testBroadcastID', name: 'message'}}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_RUNNING);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('When > hat - loudness', t => {
|
||||
const rt = new Runtime();
|
||||
rt.audioEngine = {getLoudness: () => 10};
|
||||
const e = new Event(rt);
|
||||
const args = {
|
||||
WHENGREATERTHANMENU: 'LOUDNESS',
|
||||
VALUE: '11'
|
||||
};
|
||||
t.equal(e.hatGreaterThanPredicate(args), false);
|
||||
args.VALUE = '5';
|
||||
t.equal(e.hatGreaterThanPredicate(args), true);
|
||||
t.end();
|
||||
});
|
||||
277
scratch-vm/test/unit/blocks_looks.js
Normal file
277
scratch-vm/test/unit/blocks_looks.js
Normal file
@@ -0,0 +1,277 @@
|
||||
const test = require('tap').test;
|
||||
const Looks = require('../../src/blocks/scratch3_looks');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Sprite = require('../../src/sprites/sprite.js');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target.js');
|
||||
const util = {
|
||||
target: {
|
||||
currentCostume: 0, // Internally, current costume is 0 indexed
|
||||
getCostumes: function () {
|
||||
return this.sprite.costumes;
|
||||
},
|
||||
sprite: {
|
||||
costumes: [
|
||||
{name: 'first name'},
|
||||
{name: 'second name'},
|
||||
{name: 'third name'}
|
||||
]
|
||||
},
|
||||
_customState: {},
|
||||
getCustomState: () => util.target._customState
|
||||
}
|
||||
};
|
||||
|
||||
const fakeRuntime = {
|
||||
getTargetForStage: () => util.target, // Just return the dummy target above.
|
||||
on: () => {} // Stub out listener methods used in constructor.
|
||||
};
|
||||
const blocks = new Looks(fakeRuntime);
|
||||
|
||||
/**
|
||||
* Test which costume index the `switch costume`
|
||||
* block will jump to given an argument and array
|
||||
* of costume names. Works for backdrops if isStage is set.
|
||||
*
|
||||
* @param {string[]} costumes List of costume names as strings
|
||||
* @param {string|number|boolean} arg The argument to provide to the block.
|
||||
* @param {number} [currentCostume=1] The 1-indexed default costume for the sprite to start at.
|
||||
* @param {boolean} [isStage=false] Whether the sprite is the stage
|
||||
* @return {number} The 1-indexed costume index on which the sprite lands.
|
||||
*/
|
||||
const testCostume = (costumes, arg, currentCostume = 1, isStage = false) => {
|
||||
const rt = new Runtime();
|
||||
const looks = new Looks(rt);
|
||||
|
||||
const sprite = new Sprite(null, rt);
|
||||
const target = new RenderedTarget(sprite, rt);
|
||||
|
||||
sprite.costumes = costumes.map(name => ({name: name}));
|
||||
target.currentCostume = currentCostume - 1; // Convert to 0-indexed.
|
||||
|
||||
if (isStage) {
|
||||
target.isStage = true;
|
||||
rt.addTarget(target);
|
||||
looks.switchBackdrop({BACKDROP: arg}, {target});
|
||||
} else {
|
||||
looks.switchCostume({COSTUME: arg}, {target});
|
||||
}
|
||||
|
||||
return target.currentCostume + 1; // Convert to 1-indexed.
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test which backdrop index the `switch backdrop`
|
||||
* block will jump to given an argument and array
|
||||
* of backdrop names.
|
||||
*
|
||||
* @param {string[]} backdrops List of backdrop names as strings
|
||||
* @param {string|number|boolean} arg The argument to provide to the block.
|
||||
* @param {number} [currentCostume=1] The 1-indexed default backdrop for the stage to start at.
|
||||
* @return {number} The 1-indexed backdrop index on which the stage lands.
|
||||
*/
|
||||
const testBackdrop = (backdrops, arg, currentCostume = 1) => testCostume(backdrops, arg, currentCostume, true);
|
||||
|
||||
test('switch costume block runs correctly', t => {
|
||||
// Non-existant costumes do nothing
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'e', 3), 3);
|
||||
|
||||
// Numeric arguments are always the costume index
|
||||
// String arguments are treated as costume names, and coerced to
|
||||
// a costume index as a fallback
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', '2'], 2), 2);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', '2'], '2'), 4);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c'], '2'), 2);
|
||||
|
||||
// 'previous costume' and 'next costume' increment/decrement
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'previous costume', 3), 2);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'next costume', 2), 3);
|
||||
|
||||
// 'previous costume' and 'next costume' can be overriden
|
||||
t.strictEqual(testCostume(['a', 'previous costume', 'c', 'd'], 'previous costume'), 2);
|
||||
t.strictEqual(testCostume(['next costume', 'b', 'c', 'd'], 'next costume'), 1);
|
||||
|
||||
// NaN, Infinity, and true are the first costume
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], NaN, 2), 1);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], true, 2), 1);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], Infinity, 2), 1);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], -Infinity, 2), 1);
|
||||
|
||||
// 'previous backdrop' and 'next backdrop' have no effect
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'previous backdrop', 3), 3);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'next backdrop', 3), 3);
|
||||
|
||||
// Strings with no digits are not numeric
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], ' ', 2), 2);
|
||||
|
||||
// False is 0 (the last costume)
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], false), 4);
|
||||
|
||||
// Booleans are costume names where possible.
|
||||
t.strictEqual(testCostume(['a', 'true', 'false', 'd'], false), 3);
|
||||
t.strictEqual(testCostume(['a', 'true', 'false', 'd'], true), 2);
|
||||
|
||||
// Costume indices should wrap around.
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], -1), 3);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], -4), 4);
|
||||
t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 10), 2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('switch backdrop block runs correctly', t => {
|
||||
// Non-existant backdrops do nothing
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'e', 3), 3);
|
||||
|
||||
// Difference between string and numeric arguments
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', '2'], 2), 2);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', '2'], '2'), 4);
|
||||
|
||||
// 'previous backdrop' and 'next backdrop' increment/decrement
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'previous backdrop', 3), 2);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'next backdrop', 2), 3);
|
||||
|
||||
// 'previous backdrop', 'previous backdrop', 'random backdrop' can be overriden
|
||||
// Test is deterministic since 'random backdrop' will not pick the same backdrop as currently selected
|
||||
t.strictEqual(testBackdrop(['a', 'previous backdrop', 'c', 'd'], 'previous backdrop', 4), 2);
|
||||
t.strictEqual(testBackdrop(['next backdrop', 'b', 'c', 'd'], 'next backdrop', 3), 1);
|
||||
t.strictEqual(testBackdrop(['random backdrop', 'b', 'c', 'd'], 'random backdrop'), 1);
|
||||
|
||||
// NaN, Infinity, and true are the first costume
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], NaN, 2), 1);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], true, 2), 1);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], Infinity, 2), 1);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -Infinity, 2), 1);
|
||||
|
||||
// 'previous costume' and 'next costume' have no effect
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'previous costume', 3), 3);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'next costume', 3), 3);
|
||||
|
||||
// Strings with no digits are not numeric
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], ' ', 2), 2);
|
||||
|
||||
// False is 0 (the last costume)
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], false), 4);
|
||||
|
||||
// Booleans are backdrop names where possible.
|
||||
t.strictEqual(testBackdrop(['a', 'true', 'false', 'd'], false), 3);
|
||||
t.strictEqual(testBackdrop(['a', 'true', 'false', 'd'], true), 2);
|
||||
|
||||
// Backdrop indices should wrap around.
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -1), 3);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -4), 4);
|
||||
t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 10), 2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getCostumeNumberName returns 1-indexed costume number', t => {
|
||||
util.target.currentCostume = 0; // This is 0-indexed.
|
||||
const args = {NUMBER_NAME: 'number'};
|
||||
const number = blocks.getCostumeNumberName(args, util);
|
||||
t.strictEqual(number, 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getCostumeNumberName can return costume name', t => {
|
||||
util.target.currentCostume = 0; // This is 0-indexed.
|
||||
const args = {NUMBER_NAME: 'name'};
|
||||
const name = blocks.getCostumeNumberName(args, util);
|
||||
t.strictEqual(name, 'first name');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getBackdropNumberName returns 1-indexed costume number', t => {
|
||||
util.target.currentCostume = 2; // This is 0-indexed.
|
||||
const args = {NUMBER_NAME: 'number'};
|
||||
const number = blocks.getBackdropNumberName(args, util);
|
||||
t.strictEqual(number, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getBackdropNumberName can return costume name', t => {
|
||||
util.target.currentCostume = 2; // This is 0-indexed.
|
||||
const args = {NUMBER_NAME: 'name'};
|
||||
const number = blocks.getBackdropNumberName(args, util);
|
||||
t.strictEqual(number, 'third name');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('numbers should be rounded properly in say/think', t => {
|
||||
const rt = new Runtime();
|
||||
const looks = new Looks(rt);
|
||||
|
||||
let expectedSayString;
|
||||
|
||||
rt.addListener('SAY', () => {
|
||||
const bubbleState = util.target.getCustomState(Looks.STATE_KEY);
|
||||
t.strictEqual(bubbleState.text, expectedSayString);
|
||||
});
|
||||
|
||||
expectedSayString = '3.14';
|
||||
looks.say({MESSAGE: 3.14159}, util, 'say bubble should round to 2 decimal places');
|
||||
looks.think({MESSAGE: 3.14159}, util, 'think bubble should round to 2 decimal places');
|
||||
|
||||
expectedSayString = '3';
|
||||
looks.say({MESSAGE: 3}, util, 'say bubble should not add decimal places to integers');
|
||||
looks.think({MESSAGE: 3}, util, 'think bubble should not add decimal places to integers');
|
||||
|
||||
expectedSayString = '3.10';
|
||||
looks.say({MESSAGE: 3.1}, util, 'say bubble should round to 2 decimal places, even if only 1 is needed');
|
||||
looks.think({MESSAGE: 3.1}, util, 'think bubble should round to 2 decimal places, even if only 1 is needed');
|
||||
|
||||
expectedSayString = '0.00125';
|
||||
looks.say({MESSAGE: 0.00125}, util, 'say bubble should not round if it would display small numbers as 0');
|
||||
looks.think({MESSAGE: 0.00125}, util, 'think bubble should not round if it would display small numbers as 0');
|
||||
|
||||
expectedSayString = '1.99999';
|
||||
looks.say({MESSAGE: '1.99999'}, util, 'say bubble should not round strings');
|
||||
looks.think({MESSAGE: '1.99999'}, util, 'think bubble should not round strings');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('clamp graphic effects', t => {
|
||||
const rt = new Runtime();
|
||||
const looks = new Looks(rt);
|
||||
const expectedValues = {
|
||||
brightness: {high: 100, low: -100},
|
||||
ghost: {high: 100, low: 0},
|
||||
color: {high: 500, low: -500},
|
||||
fisheye: {high: 500, low: -500},
|
||||
whirl: {high: 500, low: -500},
|
||||
pixelate: {high: 500, low: -500},
|
||||
mosaic: {high: 500, low: -500}
|
||||
};
|
||||
const args = [
|
||||
{EFFECT: 'brightness', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'brightness', VALUE: -500, CLAMP: 'low'},
|
||||
{EFFECT: 'ghost', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'ghost', VALUE: -500, CLAMP: 'low'},
|
||||
{EFFECT: 'color', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'color', VALUE: -500, CLAMP: 'low'},
|
||||
{EFFECT: 'fisheye', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'fisheye', VALUE: -500, CLAMP: 'low'},
|
||||
{EFFECT: 'whirl', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'whirl', VALUE: -500, CLAMP: 'low'},
|
||||
{EFFECT: 'pixelate', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'pixelate', VALUE: -500, CLAMP: 'low'},
|
||||
{EFFECT: 'mosaic', VALUE: 500, CLAMP: 'high'},
|
||||
{EFFECT: 'mosaic', VALUE: -500, CLAMP: 'low'}
|
||||
];
|
||||
|
||||
util.target.setEffect = function (effectName, actualValue) {
|
||||
const clamp = actualValue > 0 ? 'high' : 'low';
|
||||
rt.emit(effectName + clamp, effectName, actualValue);
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
rt.addListener(arg.EFFECT + arg.CLAMP, (effectName, actualValue) => {
|
||||
const expected = expectedValues[arg.EFFECT][arg.CLAMP];
|
||||
t.strictEqual(actualValue, expected);
|
||||
});
|
||||
|
||||
looks.setEffect(arg, util);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
26
scratch-vm/test/unit/blocks_motion.js
Normal file
26
scratch-vm/test/unit/blocks_motion.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const test = require('tap').test;
|
||||
const Motion = require('../../src/blocks/scratch3_motion');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Sprite = require('../../src/sprites/sprite.js');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target.js');
|
||||
|
||||
test('getPrimitives', t => {
|
||||
const rt = new Runtime();
|
||||
const motion = new Motion(rt);
|
||||
t.type(motion.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Coordinates have limited precision', t => {
|
||||
const rt = new Runtime();
|
||||
const motion = new Motion(rt);
|
||||
const sprite = new Sprite(null, rt);
|
||||
const target = new RenderedTarget(sprite, rt);
|
||||
const util = {target};
|
||||
|
||||
motion.goToXY({X: 0.999999999, Y: 0.999999999}, util);
|
||||
|
||||
t.equals(motion.getX({}, util), 1);
|
||||
t.equals(motion.getY({}, util), 1);
|
||||
t.end();
|
||||
});
|
||||
188
scratch-vm/test/unit/blocks_operators.js
Normal file
188
scratch-vm/test/unit/blocks_operators.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const test = require('tap').test;
|
||||
const Operators = require('../../src/blocks/scratch3_operators');
|
||||
|
||||
const blocks = new Operators(null);
|
||||
|
||||
test('getPrimitives', t => {
|
||||
t.type(blocks.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('add', t => {
|
||||
t.strictEqual(blocks.add({NUM1: '1', NUM2: '1'}), 2);
|
||||
t.strictEqual(blocks.add({NUM1: 'foo', NUM2: 'bar'}), 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('subtract', t => {
|
||||
t.strictEqual(blocks.subtract({NUM1: '1', NUM2: '1'}), 0);
|
||||
t.strictEqual(blocks.subtract({NUM1: 'foo', NUM2: 'bar'}), 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('multiply', t => {
|
||||
t.strictEqual(blocks.multiply({NUM1: '2', NUM2: '2'}), 4);
|
||||
t.strictEqual(blocks.multiply({NUM1: 'foo', NUM2: 'bar'}), 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('divide', t => {
|
||||
t.strictEqual(blocks.divide({NUM1: '2', NUM2: '2'}), 1);
|
||||
t.ok(isNaN(blocks.divide({NUM1: 'foo', NUM2: 'bar'}))); // @todo
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lt', t => {
|
||||
t.strictEqual(blocks.lt({OPERAND1: '1', OPERAND2: '2'}), true);
|
||||
t.strictEqual(blocks.lt({OPERAND1: '2', OPERAND2: '1'}), false);
|
||||
t.strictEqual(blocks.lt({OPERAND1: '1', OPERAND2: '1'}), false);
|
||||
t.strictEqual(blocks.lt({OPERAND1: '10', OPERAND2: '2'}), false);
|
||||
t.strictEqual(blocks.lt({OPERAND1: 'a', OPERAND2: 'z'}), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('equals', t => {
|
||||
t.strictEqual(blocks.equals({OPERAND1: '1', OPERAND2: '2'}), false);
|
||||
t.strictEqual(blocks.equals({OPERAND1: '2', OPERAND2: '1'}), false);
|
||||
t.strictEqual(blocks.equals({OPERAND1: '1', OPERAND2: '1'}), true);
|
||||
t.strictEqual(blocks.equals({OPERAND1: 'あ', OPERAND2: 'ア'}), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('gt', t => {
|
||||
t.strictEqual(blocks.gt({OPERAND1: '1', OPERAND2: '2'}), false);
|
||||
t.strictEqual(blocks.gt({OPERAND1: '2', OPERAND2: '1'}), true);
|
||||
t.strictEqual(blocks.gt({OPERAND1: '1', OPERAND2: '1'}), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('and', t => {
|
||||
t.strictEqual(blocks.and({OPERAND1: true, OPERAND2: true}), true);
|
||||
t.strictEqual(blocks.and({OPERAND1: true, OPERAND2: false}), false);
|
||||
t.strictEqual(blocks.and({OPERAND1: false, OPERAND2: false}), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('or', t => {
|
||||
t.strictEqual(blocks.or({OPERAND1: true, OPERAND2: true}), true);
|
||||
t.strictEqual(blocks.or({OPERAND1: true, OPERAND2: false}), true);
|
||||
t.strictEqual(blocks.or({OPERAND1: false, OPERAND2: false}), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('not', t => {
|
||||
t.strictEqual(blocks.not({OPERAND: true}), false);
|
||||
t.strictEqual(blocks.not({OPERAND: false}), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('random', t => {
|
||||
const min = 0;
|
||||
const max = 100;
|
||||
const result = blocks.random({FROM: min, TO: max});
|
||||
t.ok(result >= min);
|
||||
t.ok(result <= max);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('random - equal', t => {
|
||||
const min = 1;
|
||||
const max = 1;
|
||||
t.strictEqual(blocks.random({FROM: min, TO: max}), min);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('random - decimal', t => {
|
||||
const min = 0.1;
|
||||
const max = 10;
|
||||
const result = blocks.random({FROM: min, TO: max});
|
||||
t.ok(result >= min);
|
||||
t.ok(result <= max);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('random - int', t => {
|
||||
const min = 0;
|
||||
const max = 10;
|
||||
const result = blocks.random({FROM: min, TO: max});
|
||||
t.ok(result >= min);
|
||||
t.ok(result <= max);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('random - reverse', t => {
|
||||
const min = 0;
|
||||
const max = 10;
|
||||
const result = blocks.random({FROM: max, TO: min});
|
||||
t.ok(result >= min);
|
||||
t.ok(result <= max);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('join', t => {
|
||||
t.strictEqual(blocks.join({STRING1: 'foo', STRING2: 'bar'}), 'foobar');
|
||||
t.strictEqual(blocks.join({STRING1: '1', STRING2: '2'}), '12');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('letterOf', t => {
|
||||
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 0}), '');
|
||||
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 1}), 'f');
|
||||
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 2}), 'o');
|
||||
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 3}), 'o');
|
||||
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 4}), '');
|
||||
t.strictEqual(blocks.letterOf({STRING: 'foo', LETTER: 'bar'}), '');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('length', t => {
|
||||
t.strictEqual(blocks.length({STRING: ''}), 0);
|
||||
t.strictEqual(blocks.length({STRING: 'foo'}), 3);
|
||||
t.strictEqual(blocks.length({STRING: '1'}), 1);
|
||||
t.strictEqual(blocks.length({STRING: '100'}), 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('contains', t => {
|
||||
t.strictEqual(blocks.contains({STRING1: 'hello world', STRING2: 'hello'}), true);
|
||||
t.strictEqual(blocks.contains({STRING1: 'foo', STRING2: 'bar'}), false);
|
||||
t.strictEqual(blocks.contains({STRING1: 'HeLLo world', STRING2: 'hello'}), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mod', t => {
|
||||
t.strictEqual(blocks.mod({NUM1: 1, NUM2: 1}), 0);
|
||||
t.strictEqual(blocks.mod({NUM1: 3, NUM2: 6}), 3);
|
||||
t.strictEqual(blocks.mod({NUM1: -3, NUM2: 6}), 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('round', t => {
|
||||
t.strictEqual(blocks.round({NUM: 1}), 1);
|
||||
t.strictEqual(blocks.round({NUM: 1.1}), 1);
|
||||
t.strictEqual(blocks.round({NUM: 1.5}), 2);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mathop', t => {
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'abs', NUM: -1}), 1);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'floor', NUM: 1.5}), 1);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'ceiling', NUM: 0.1}), 1);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'sqrt', NUM: 1}), 1);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'sin', NUM: 1}), 0.0174524064);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'sin', NUM: 90}), 1);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'cos', NUM: 1}), 0.9998476952);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'cos', NUM: 180}), -1);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'tan', NUM: 1}), 0.0174550649);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'tan', NUM: 90}), Infinity);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'tan', NUM: 180}), 0);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'asin', NUM: 1}), 90);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'acos', NUM: 1}), 0);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'atan', NUM: 1}), 45);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'ln', NUM: 1}), 0);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'log', NUM: 1}), 0);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'e ^', NUM: 1}), 2.718281828459045);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: '10 ^', NUM: 1}), 10);
|
||||
t.strictEqual(blocks.mathop({OPERATOR: 'undefined', NUM: 1}), 0);
|
||||
t.end();
|
||||
});
|
||||
327
scratch-vm/test/unit/blocks_operators_infinity.js
Normal file
327
scratch-vm/test/unit/blocks_operators_infinity.js
Normal file
@@ -0,0 +1,327 @@
|
||||
const test = require('tap').test;
|
||||
const Operators = require('../../src/blocks/scratch3_operators');
|
||||
|
||||
const blocks = new Operators(null);
|
||||
|
||||
test('divide: (1) / (0) = Infinity', t => {
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: '1', NUM2: '0'}), Infinity, '1 / 0 = Infinity'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('divide: division with Infinity', t => {
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 'Infinity', NUM2: 111}), Infinity, '"Infinity" / 111 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 'INFINITY', NUM2: 222}), 0, '"INFINITY" / 222 = 0'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: Infinity, NUM2: 333}), Infinity, 'Infinity / 333 = Infinity'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 111, NUM2: 'Infinity'}), 0, '111 / "Infinity" = 0'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 222, NUM2: 'INFINITY'}), Infinity, '222 / "INFINITY" = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 333, NUM2: Infinity}), 0, '333 / Infinity = 0'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: '-Infinity', NUM2: 111}), -Infinity, '"-Infinity" / 111 = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: '-INFINITY', NUM2: 222}), 0, '"-INFINITY" / 222 = 0'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: -Infinity, NUM2: 333}), -Infinity, '-Infinity / 333 = -Infinity'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 111, NUM2: '-Infinity'}), 0, '111 / "-Infinity" = 0'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 222, NUM2: '-INFINITY'}), Infinity, '222 / "-INFINITY" = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.divide({NUM1: 333, NUM2: -Infinity}), 0, '333 / -Infinity = 0'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('multiply: multiply Infinity with numbers', t => {
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: 'Infinity', NUM2: 111}), Infinity, '"Infinity" * 111 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: 'INFINITY', NUM2: 222}), 0, '"INFINITY" * 222 = 0'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: Infinity, NUM2: 333}), Infinity, 'Infinity * 333 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: '-Infinity', NUM2: 111}), -Infinity, '"-Infinity" * 111 = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: '-INFINITY', NUM2: 222}), 0, '"-INFINITY" * 222 = 0'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: -Infinity, NUM2: 333}), -Infinity, '-Infinity * 333 = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.multiply({NUM1: -Infinity, NUM2: Infinity}), -Infinity, '-Infinity * Infinity = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
Number.isNaN(blocks.multiply({NUM1: Infinity, NUM2: 0})), true, 'Infinity * 0 = NaN'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('add: add Infinity to a number', t => {
|
||||
|
||||
t.strictEqual(
|
||||
blocks.add({NUM1: 'Infinity', NUM2: 111}), Infinity, '"Infinity" + 111 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.add({NUM1: 'INFINITY', NUM2: 222}), 222, '"INFINITY" + 222 = 222'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.add({NUM1: Infinity, NUM2: 333}), Infinity, 'Infinity + 333 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.add({NUM1: '-Infinity', NUM2: 111}), -Infinity, '"-Infinity" + 111 = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.add({NUM1: '-INFINITY', NUM2: 222}), 222, '"-INFINITY" + 222 = 222'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.add({NUM1: -Infinity, NUM2: 333}), -Infinity, '-Infinity + 333 = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
Number.isNaN(blocks.add({NUM1: -Infinity, NUM2: Infinity})), true, '-Infinity + Infinity = NaN'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('subtract: subtract Infinity with a number', t => {
|
||||
|
||||
t.strictEqual(
|
||||
blocks.subtract({NUM1: 'Infinity', NUM2: 111}), Infinity, '"Infinity" - 111 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.subtract({NUM1: 'INFINITY', NUM2: 222}), -222, '"INFINITY" - 222 = -222'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.subtract({NUM1: Infinity, NUM2: 333}), Infinity, 'Infinity - 333 = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.subtract({NUM1: 111, NUM2: 'Infinity'}), -Infinity, '111 - "Infinity" = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.subtract({NUM1: 222, NUM2: 'INFINITY'}), 222, '222 - "INFINITY" = 222'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.subtract({NUM1: 333, NUM2: Infinity}), -Infinity, '333 - Infinity = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
Number.isNaN(blocks.subtract({NUM1: Infinity, NUM2: Infinity})), true, 'Infinity - Infinity = NaN'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('equals: compare string infinity and numeric Infinity', t => {
|
||||
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'Infinity', OPERAND2: 'INFINITY'}), true, '"Infinity" = "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'INFINITY', OPERAND2: 'Infinity'}), true, '"INFINITY" = "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'Infinity', OPERAND2: 'Infinity'}), true, '"Infinity" = "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'INFINITY', OPERAND2: 'INFINITY'}), true, '"INFINITY" = "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'INFINITY', OPERAND2: 'infinity'}), true, '"INFINITY" = "infinity"'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: Infinity, OPERAND2: Infinity}), true, 'Infinity = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'Infinity', OPERAND2: Infinity}), true, '"Infinity" = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'INFINITY', OPERAND2: Infinity}), true, '"INFINITY" = Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: Infinity, OPERAND2: 'Infinity'}), true, 'Infinity = "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: Infinity, OPERAND2: 'INFINITY'}), true, 'Infinity = "INFINITY'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('equals: compare string negative infinity and numeric negative Infinity', t => {
|
||||
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-Infinity', OPERAND2: '-INFINITY'}), true, '"-Infinity" = "-INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: '-Infinity'}), true, '"-INFINITY" = "-Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-Infinity', OPERAND2: '-Infinity'}), true, '"-Infinity" = "-Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: '-INFINITY'}), true, '"-INFINITY" = "-INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: '-infinity'}), true, '"-INFINITY" = "-infinity"'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: -Infinity, OPERAND2: -Infinity}), true, '-Infinity = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-Infinity', OPERAND2: -Infinity}), true, '"-Infinity" = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: -Infinity}), true, '"-INFINITY" = -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: -Infinity, OPERAND2: '-Infinity'}), true, '-Infinity = "-Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: -Infinity, OPERAND2: '-INFINITY'}), true, '-Infinity = "-INFINITY'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
|
||||
test('equals: compare negative to postive string and numeric Infinity', t => {
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-Infinity', OPERAND2: 'Infinity'}), false, '"-Infinity" != "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-Infinity', OPERAND2: 'INFINITY'}), false, '"-infinity" != "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: 'Infinity'}), false, '"-INFINITY" != "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: 'INFINITY'}), false, '"-INFINITY" != "INFINITY"'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-Infinity', OPERAND2: Infinity}), false, '"-Infinity" != Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: '-INFINITY', OPERAND2: Infinity}), false, '"-INFINITY" != Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'Infinity', OPERAND2: -Infinity}), false, '"Infinity" != -Infinity'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: 'INFINITY', OPERAND2: -Infinity}), false, '"INFINITY" != -Infinity'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.equals({OPERAND1: Infinity, OPERAND2: -Infinity}), false, 'Infinity != -Infinity'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('less than: compare string infinity and numeric Infinity', t => {
|
||||
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 'Infinity', OPERAND2: 'INFINITY'}), false, '"Infinity" !< "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 'INFINITY', OPERAND2: Infinity}), false, '"INFINITY" !< "Infinity"'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: '-INFINITY', OPERAND2: 'INFINITY'}), true, '"-Infinity" < "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: -Infinity, OPERAND2: 'INFINITY'}), true, '-Infinity < "INFINITY"'
|
||||
);
|
||||
|
||||
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 'Infinity', OPERAND2: 111}), false, '"Infinity" !< 111'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 'INFINITY', OPERAND2: 222}), false, '"INFINITY" !< 222'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: Infinity, OPERAND2: 333}), false, 'Infinity !< 333'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 111, OPERAND2: 'Infinity'}), true, '111 < "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 222, OPERAND2: 'INFINITY'}), true, '222 < "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.lt({OPERAND1: 333, OPERAND2: Infinity}), true, '333 < Infinity'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('more than: compare string infinity and numeric Infinity', t => {
|
||||
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 'Infinity', OPERAND2: 'INFINITY'}), false, '"Infinity" !> "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 'INFINITY', OPERAND2: Infinity}), false, '"INFINITY" !> "Infinity"'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 'INFINITY', OPERAND2: '-INFINITY'}), true, '"Infinity" < "-INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: Infinity, OPERAND2: '-INFINITY'}), true, 'Infinity < "-INFINITY"'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 'Infinity', OPERAND2: 111}), true, '"Infinity" > 111'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 'INFINITY', OPERAND2: 222}), true, '"INFINITY" > 222'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: Infinity, OPERAND2: 333}), true, 'Infinity > 333'
|
||||
);
|
||||
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 111, OPERAND2: 'Infinity'}), false, '111 !> "Infinity"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 222, OPERAND2: 'INFINITY'}), false, '222 !> "INFINITY"'
|
||||
);
|
||||
t.strictEqual(
|
||||
blocks.gt({OPERAND1: 333, OPERAND2: Infinity}), false, '333 !> Infinity'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
32
scratch-vm/test/unit/blocks_pen_tw.js
Normal file
32
scratch-vm/test/unit/blocks_pen_tw.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Scratch3PenBlocks = require('../../src/extensions/scratch3_pen/index');
|
||||
|
||||
test('_clampPenSize', t => {
|
||||
const rt = new Runtime();
|
||||
const pen = new Scratch3PenBlocks(rt);
|
||||
|
||||
t.equal(pen._clampPenSize(-1), 1);
|
||||
t.equal(pen._clampPenSize(0), 1);
|
||||
t.equal(pen._clampPenSize(0.25), 1);
|
||||
t.equal(pen._clampPenSize(1), 1);
|
||||
t.equal(pen._clampPenSize(10), 10);
|
||||
t.equal(pen._clampPenSize(1000), 1000);
|
||||
t.equal(pen._clampPenSize(1200), 1200);
|
||||
t.equal(pen._clampPenSize(1201), 1200);
|
||||
|
||||
rt.setRuntimeOptions({
|
||||
miscLimits: false
|
||||
});
|
||||
t.equal(pen._clampPenSize(-1), 0);
|
||||
t.equal(pen._clampPenSize(0), 0);
|
||||
t.equal(pen._clampPenSize(0.25), 0.25);
|
||||
t.equal(pen._clampPenSize(1), 1);
|
||||
t.equal(pen._clampPenSize(10), 10);
|
||||
t.equal(pen._clampPenSize(1000), 1000);
|
||||
t.equal(pen._clampPenSize(1200), 1200);
|
||||
t.equal(pen._clampPenSize(1201), 1201);
|
||||
|
||||
t.end();
|
||||
});
|
||||
28
scratch-vm/test/unit/blocks_procedures.js
Normal file
28
scratch-vm/test/unit/blocks_procedures.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const test = require('tap').test;
|
||||
const Procedures = require('../../src/blocks/scratch3_procedures');
|
||||
|
||||
const blocks = new Procedures(null);
|
||||
|
||||
test('getPrimitives', t => {
|
||||
t.type(blocks.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Originally inspired by https://github.com/scratchfoundation/scratch-gui/issues/809
|
||||
test('calling a custom block with no definition does not throw', t => {
|
||||
const args = {
|
||||
mutation: {
|
||||
proccode: 'undefined proc'
|
||||
}
|
||||
};
|
||||
const util = {
|
||||
getProcedureParamNamesIdsAndDefaults: () => null,
|
||||
stackFrame: {
|
||||
executed: false
|
||||
}
|
||||
};
|
||||
t.doesNotThrow(() => {
|
||||
blocks.call(args, util);
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
285
scratch-vm/test/unit/blocks_sensing.js
Normal file
285
scratch-vm/test/unit/blocks_sensing.js
Normal file
@@ -0,0 +1,285 @@
|
||||
const test = require('tap').test;
|
||||
const Sensing = require('../../src/blocks/scratch3_sensing');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const BlockUtility = require('../../src/engine/block-utility');
|
||||
|
||||
test('getPrimitives', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
t.type(s.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ask and answer with a hidden target', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
// Test is written out of order because of promises, follow the (#) comments.
|
||||
rt.addListener('QUESTION', question => {
|
||||
// (2) Assert the question is correct, then emit the answer
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
rt.emit('ANSWER', expectedAnswer);
|
||||
});
|
||||
|
||||
// (1) Emit the question.
|
||||
const promise = s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
|
||||
// (3) Ask block resolves after the answer is emitted.
|
||||
promise.then(() => {
|
||||
t.strictEqual(s.getAnswer(), expectedAnswer);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('ask and stop all dismisses question', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
|
||||
let call = 0;
|
||||
|
||||
rt.addListener('QUESTION', question => {
|
||||
if (call === 0) {
|
||||
// (2) Assert the question was passed.
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
} else if (call === 1) {
|
||||
// (4) Assert the question was dismissed.
|
||||
t.strictEqual(question, null);
|
||||
t.end();
|
||||
}
|
||||
call += 1;
|
||||
});
|
||||
|
||||
// (1) Emit the question.
|
||||
s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
// (3) Emit the stop all event.
|
||||
rt.stopAll();
|
||||
});
|
||||
|
||||
test('ask and stop other scripts dismisses if it is the last question', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false, sprite: {}, getCustomState: () => ({})}, thread: {}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
|
||||
let call = 0;
|
||||
|
||||
rt.addListener('QUESTION', question => {
|
||||
if (call === 0) {
|
||||
// (2) Assert the question was passed.
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
} else if (call === 1) {
|
||||
// (4) Assert the question was dismissed.
|
||||
t.strictEqual(question, null);
|
||||
t.end();
|
||||
}
|
||||
call += 1;
|
||||
});
|
||||
|
||||
// (1) Emit the questions.
|
||||
s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
// (3) Emit the stop for target event.
|
||||
rt.stopForTarget(util.target, util.thread);
|
||||
});
|
||||
|
||||
test('ask and stop other scripts asks next question', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false, sprite: {}, getCustomState: () => ({})}, thread: {}};
|
||||
const util2 = {target: {visible: false, sprite: {}, getCustomState: () => ({})}, thread: {}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
const nextQuestion = 'a followup';
|
||||
|
||||
let call = 0;
|
||||
|
||||
rt.addListener('QUESTION', question => {
|
||||
if (call === 0) {
|
||||
// (2) Assert the question was passed.
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
} else if (call === 1) {
|
||||
// (4) Assert the next question was passed.
|
||||
t.strictEqual(question, nextQuestion);
|
||||
t.end();
|
||||
}
|
||||
call += 1;
|
||||
});
|
||||
|
||||
// (1) Emit the questions.
|
||||
s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
s.askAndWait({QUESTION: nextQuestion}, util2);
|
||||
// (3) Emit the stop for target event.
|
||||
rt.stopForTarget(util.target, util.thread);
|
||||
});
|
||||
|
||||
test('ask and answer with a visible target', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: true}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
rt.removeAllListeners('SAY'); // Prevent say blocks from executing
|
||||
|
||||
rt.addListener('SAY', (target, type, question) => {
|
||||
// Should emit SAY with the question
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
});
|
||||
|
||||
rt.addListener('QUESTION', question => {
|
||||
// Question should be blank for a visible target
|
||||
t.strictEqual(question, '');
|
||||
|
||||
// Remove the say listener and add a new one to assert bubble is cleared
|
||||
// by setting say to empty string after answer is received.
|
||||
rt.removeAllListeners('SAY');
|
||||
rt.addListener('SAY', (target, type, text) => {
|
||||
t.strictEqual(text, '');
|
||||
t.end();
|
||||
});
|
||||
rt.emit('ANSWER', expectedAnswer);
|
||||
});
|
||||
|
||||
s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
});
|
||||
|
||||
test('answer gets reset when runtime is disposed', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false}};
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
rt.addListener('QUESTION', () => rt.emit('ANSWER', expectedAnswer));
|
||||
const promise = s.askAndWait({QUESTION: ''}, util);
|
||||
|
||||
promise.then(() => t.strictEqual(s.getAnswer(), expectedAnswer))
|
||||
.then(() => rt.dispose())
|
||||
.then(() => {
|
||||
t.strictEqual(s.getAnswer(), '');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('set drag mode', t => {
|
||||
const runtime = new Runtime();
|
||||
runtime.requestTargetsUpdate = () => {}; // noop for testing
|
||||
const sensing = new Sensing(runtime);
|
||||
const s = new Sprite(null, runtime);
|
||||
const rt = new RenderedTarget(s, runtime);
|
||||
|
||||
sensing.setDragMode({DRAG_MODE: 'not draggable'}, {target: rt});
|
||||
t.strictEqual(rt.draggable, false);
|
||||
|
||||
sensing.setDragMode({DRAG_MODE: 'draggable'}, {target: rt});
|
||||
t.strictEqual(rt.draggable, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('get loudness with caching', t => {
|
||||
const rt = new Runtime();
|
||||
const sensing = new Sensing(rt);
|
||||
|
||||
// It should report -1 when audio engine is not available.
|
||||
t.strictEqual(sensing.getLoudness(), -1);
|
||||
|
||||
// Stub the audio engine with its getLoudness function, and set up different
|
||||
// values to simulate it changing over time.
|
||||
const firstLoudness = 1;
|
||||
const secondLoudness = 2;
|
||||
let simulatedLoudness = firstLoudness;
|
||||
rt.audioEngine = {getLoudness: () => simulatedLoudness};
|
||||
|
||||
// It should report -1 when current step time is null.
|
||||
// TW: The concept of a null current step time is inherently flawed and removed in TurboWarp.
|
||||
// t.strictEqual(sensing.getLoudness(), -1);
|
||||
|
||||
// Stub the current step time.
|
||||
rt.currentStepTime = 1000 / 30;
|
||||
|
||||
// The first time it works, it should report the result from the stubbed audio engine.
|
||||
t.strictEqual(sensing.getLoudness(), firstLoudness);
|
||||
|
||||
// Update the simulated loudness to a new value.
|
||||
simulatedLoudness = secondLoudness;
|
||||
|
||||
// Simulate time passing by advancing the timer forward a little bit.
|
||||
// After less than a step, it should still report cached loudness.
|
||||
let simulatedTime = Date.now() + (rt.currentStepTime / 2);
|
||||
sensing._timer = {time: () => simulatedTime};
|
||||
t.strictEqual(sensing.getLoudness(), firstLoudness);
|
||||
|
||||
// Simulate more than a step passing. It should now request the value
|
||||
// from the audio engine again.
|
||||
simulatedTime += rt.currentStepTime;
|
||||
t.strictEqual(sensing.getLoudness(), secondLoudness);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('loud? boolean', t => {
|
||||
const rt = new Runtime();
|
||||
const sensing = new Sensing(rt);
|
||||
|
||||
// The simplest way to test this is to actually override the getLoudness
|
||||
// method, which isLoud uses.
|
||||
let simulatedLoudness = 0;
|
||||
sensing.getLoudness = () => simulatedLoudness;
|
||||
t.false(sensing.isLoud());
|
||||
|
||||
// Check for GREATER than 10, not equal.
|
||||
simulatedLoudness = 10;
|
||||
t.false(sensing.isLoud());
|
||||
|
||||
simulatedLoudness = 11;
|
||||
t.true(sensing.isLoud());
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('get attribute of sprite variable', t => {
|
||||
const rt = new Runtime();
|
||||
const sensing = new Sensing(rt);
|
||||
const s = new Sprite(null, rt);
|
||||
const target = new RenderedTarget(s, rt);
|
||||
const variable = {
|
||||
name: 'cars',
|
||||
value: 'trucks',
|
||||
type: ''
|
||||
};
|
||||
// Add variable to set the map (it should be empty before this).
|
||||
target.variables.anId = variable;
|
||||
rt.getSpriteTargetByName = () => target;
|
||||
t.equal(sensing.getAttributeOf({PROPERTY: 'cars'}), 'trucks');
|
||||
|
||||
t.end();
|
||||
});
|
||||
test('get attribute of variable that does not exist', t => {
|
||||
const rt = new Runtime();
|
||||
const sensing = new Sensing(rt);
|
||||
const s = new Sprite(null, rt);
|
||||
const target = new RenderedTarget(s, rt);
|
||||
rt.getTargetForStage = () => target;
|
||||
t.equal(sensing.getAttributeOf({PROPERTY: 'variableThatDoesNotExist'}), 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('username block', t => {
|
||||
const rt = new Runtime();
|
||||
const sensing = new Sensing(rt);
|
||||
const util = new BlockUtility(rt.sequencer);
|
||||
|
||||
t.equal(sensing.getUsername({}, util), '');
|
||||
t.end();
|
||||
});
|
||||
35
scratch-vm/test/unit/blocks_sound_tw.js
Normal file
35
scratch-vm/test/unit/blocks_sound_tw.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Target = require('../../src/engine/target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const Scratch3SoundBlocks = require('../../src/blocks/scratch3_sound');
|
||||
|
||||
test('effect clamping runtime option', t => {
|
||||
const rt = new Runtime();
|
||||
const target = new Target(rt);
|
||||
const sprite = new Sprite();
|
||||
target.sprite = sprite;
|
||||
const sound = new Scratch3SoundBlocks(rt);
|
||||
|
||||
sound.setEffect({
|
||||
EFFECT: 'pitch',
|
||||
VALUE: 720
|
||||
}, {
|
||||
target
|
||||
});
|
||||
t.equal(sound._getSoundState(target).effects.pitch, 360);
|
||||
|
||||
rt.setRuntimeOptions({
|
||||
miscLimits: false
|
||||
});
|
||||
sound.setEffect({
|
||||
EFFECT: 'pitch',
|
||||
VALUE: 720
|
||||
}, {
|
||||
target
|
||||
});
|
||||
t.equal(sound._getSoundState(target).effects.pitch, 720);
|
||||
|
||||
t.end();
|
||||
});
|
||||
73
scratch-vm/test/unit/blocks_sounds.js
Normal file
73
scratch-vm/test/unit/blocks_sounds.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const test = require('tap').test;
|
||||
const Sound = require('../../src/blocks/scratch3_sound');
|
||||
let playedSound;
|
||||
|
||||
const blocks = new Sound();
|
||||
const util = {
|
||||
target: {
|
||||
sprite: {
|
||||
sounds: [
|
||||
{name: 'first name', soundId: 'first soundId'},
|
||||
{name: 'second name', soundId: 'second soundId'},
|
||||
{name: 'third name', soundId: 'third soundId'},
|
||||
{name: '6', soundId: 'fourth soundId'}
|
||||
],
|
||||
soundBank: {
|
||||
playSound: (target, soundId) => (playedSound = soundId)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test('playSound with a name string works', t => {
|
||||
const args = {SOUND_MENU: 'second name'};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'second soundId');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('playSound with a number string works 1-indexed', t => {
|
||||
let args = {SOUND_MENU: '5'};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'first soundId');
|
||||
|
||||
args = {SOUND_MENU: '1'};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'first soundId');
|
||||
|
||||
args = {SOUND_MENU: '0'};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'fourth soundId');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('playSound with a number works 1-indexed', t => {
|
||||
let args = {SOUND_MENU: 5};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'first soundId');
|
||||
|
||||
args = {SOUND_MENU: 1};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'first soundId');
|
||||
|
||||
args = {SOUND_MENU: 0};
|
||||
blocks.playSound(args, util);
|
||||
t.strictEqual(playedSound, 'fourth soundId');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('playSound prioritizes sound index if given a number', t => {
|
||||
const args = {SOUND_MENU: 6};
|
||||
blocks.playSound(args, util);
|
||||
// Ignore the sound named '6', wrapClamp to the second instead
|
||||
t.strictEqual(playedSound, 'second soundId');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('playSound prioritizes sound name if given a string', t => {
|
||||
const args = {SOUND_MENU: '6'};
|
||||
blocks.playSound(args, util);
|
||||
// Use the sound named '6', which is the fourth
|
||||
t.strictEqual(playedSound, 'fourth soundId');
|
||||
t.end();
|
||||
});
|
||||
82
scratch-vm/test/unit/dispatch.js
Normal file
82
scratch-vm/test/unit/dispatch.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const DispatchTestService = require('../fixtures/dispatch-test-service');
|
||||
const Worker = require('tiny-worker');
|
||||
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
const runServiceTest = function (serviceName, t) {
|
||||
const promises = [];
|
||||
|
||||
promises.push(dispatch.call(serviceName, 'returnFortyTwo')
|
||||
.then(
|
||||
x => t.equal(x, 42),
|
||||
e => t.fail(e)
|
||||
));
|
||||
|
||||
promises.push(dispatch.call(serviceName, 'doubleArgument', 9)
|
||||
.then(
|
||||
x => t.equal(x, 18),
|
||||
e => t.fail(e)
|
||||
));
|
||||
|
||||
promises.push(dispatch.call(serviceName, 'doubleArgument', 123)
|
||||
.then(
|
||||
x => t.equal(x, 246),
|
||||
e => t.fail(e)
|
||||
));
|
||||
|
||||
// I tried using `t.rejects` here but ran into https://github.com/tapjs/node-tap/issues/384
|
||||
promises.push(dispatch.call(serviceName, 'throwException')
|
||||
.then(
|
||||
() => t.fail('exception was not propagated as expected'),
|
||||
() => t.pass('exception was propagated as expected')
|
||||
));
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
test('local', t => {
|
||||
dispatch.setService('LocalDispatchTest', new DispatchTestService())
|
||||
.catch(e => t.fail(e));
|
||||
|
||||
return runServiceTest('LocalDispatchTest', t);
|
||||
});
|
||||
|
||||
test('remote', t => {
|
||||
const fixturesDir = path.resolve(__dirname, '../fixtures');
|
||||
const shimPath = path.resolve(fixturesDir, 'dispatch-test-worker-shim.js');
|
||||
const worker = new Worker(shimPath, null, {cwd: fixturesDir});
|
||||
dispatch.addWorker(worker);
|
||||
|
||||
const waitForWorker = new Promise(resolve => {
|
||||
dispatch.setService('test', {onWorkerReady: resolve})
|
||||
.catch(e => t.fail(e));
|
||||
});
|
||||
|
||||
return waitForWorker
|
||||
.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();
|
||||
});
|
||||
220
scratch-vm/test/unit/engine_adapter.js
Normal file
220
scratch-vm/test/unit/engine_adapter.js
Normal file
@@ -0,0 +1,220 @@
|
||||
const test = require('tap').test;
|
||||
const adapter = require('../../src/engine/adapter');
|
||||
const events = require('../fixtures/events.json');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(adapter, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('invalid inputs', t => {
|
||||
let nothing = adapter('not an object');
|
||||
t.type(nothing, 'undefined');
|
||||
nothing = adapter({noxmlproperty: true});
|
||||
t.type(nothing, 'undefined');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create event', t => {
|
||||
const result = adapter(events.create);
|
||||
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 2);
|
||||
|
||||
// Outer block
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].comment, 'undefined');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].inputs.DURATION, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
|
||||
// Enclosed shadow block
|
||||
t.type(result[1].id, 'string');
|
||||
t.type(result[1].opcode, 'string');
|
||||
t.type(result[1].fields, 'object');
|
||||
t.type(result[1].inputs, 'object');
|
||||
t.type(result[1].fields.NUM, 'object');
|
||||
t.type(result[1].fields.NUM.value, '10');
|
||||
t.type(result[1].topLevel, 'boolean');
|
||||
t.equal(result[1].topLevel, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with comment', t => {
|
||||
const result = adapter(events.createComment);
|
||||
|
||||
// This test should be the same as above except that it also has a comment.
|
||||
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 2);
|
||||
|
||||
t.type(result[0].comment, 'string');
|
||||
t.equal(result[0].comment, 'aCommentId');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with branch', t => {
|
||||
const result = adapter(events.createbranch);
|
||||
// Outer block
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].inputs.SUBSTACK, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
// In branch
|
||||
const branchBlockId = result[0].inputs.SUBSTACK.block;
|
||||
const branchShadowId = result[0].inputs.SUBSTACK.shadow;
|
||||
t.type(branchBlockId, 'string');
|
||||
t.equal(branchShadowId, null);
|
||||
// Find actual branch block
|
||||
let branchBlock = null;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (result[i].id === branchBlockId) {
|
||||
branchBlock = result[i];
|
||||
}
|
||||
}
|
||||
t.type(branchBlock, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with two branches', t => {
|
||||
const result = adapter(events.createtwobranches);
|
||||
// Outer block
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].inputs.SUBSTACK, 'object');
|
||||
t.type(result[0].inputs.SUBSTACK2, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
// In branchs
|
||||
const firstBranchBlockId = result[0].inputs.SUBSTACK.block;
|
||||
const secondBranchBlockId = result[0].inputs.SUBSTACK2.block;
|
||||
t.type(firstBranchBlockId, 'string');
|
||||
t.type(secondBranchBlockId, 'string');
|
||||
const firstBranchShadowBlockId = result[0].inputs.SUBSTACK.shadow;
|
||||
const secondBranchShadowBlockId = result[0].inputs.SUBSTACK2.shadow;
|
||||
t.equal(firstBranchShadowBlockId, null);
|
||||
t.equal(secondBranchShadowBlockId, null);
|
||||
// Find actual branch blocks
|
||||
let firstBranchBlock = null;
|
||||
let secondBranchBlock = null;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (result[i].id === firstBranchBlockId) {
|
||||
firstBranchBlock = result[i];
|
||||
}
|
||||
if (result[i].id === secondBranchBlockId) {
|
||||
secondBranchBlock = result[i];
|
||||
}
|
||||
}
|
||||
t.type(firstBranchBlock, 'object');
|
||||
t.type(secondBranchBlock, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with top-level shadow', t => {
|
||||
const result = adapter(events.createtoplevelshadow);
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 1);
|
||||
|
||||
// Outer block
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with next connection', t => {
|
||||
const result = adapter(events.createwithnext);
|
||||
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 2);
|
||||
|
||||
// First block
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
t.type(result[0].next, 'string');
|
||||
t.equal(result[0].next, result[1].id);
|
||||
|
||||
// Second block
|
||||
t.type(result[1].id, 'string');
|
||||
t.type(result[1].opcode, 'string');
|
||||
t.type(result[1].fields, 'object');
|
||||
t.type(result[1].inputs, 'object');
|
||||
t.type(result[1].topLevel, 'boolean');
|
||||
t.equal(result[1].topLevel, false);
|
||||
t.equal(result[1].next, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with obscured shadow', t => {
|
||||
const result = adapter(events.createobscuredshadow);
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 4);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create variable with entity in name', t => {
|
||||
const result = adapter(events.createvariablewithentity);
|
||||
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 1);
|
||||
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].fields.VARIABLE, 'object');
|
||||
t.type(result[0].fields.VARIABLE.value, 'string');
|
||||
t.equal(result[0].fields.VARIABLE.value, 'this & that');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with invalid block xml', t => {
|
||||
// Entirely invalid block XML
|
||||
const result = adapter(events.createinvalid);
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 0);
|
||||
|
||||
// Invalid grandchild tag
|
||||
const result2 = adapter(events.createinvalidgrandchild);
|
||||
t.ok(Array.isArray(result2));
|
||||
t.equal(result2.length, 1);
|
||||
t.type(result2[0].id, 'string');
|
||||
t.equal(Object.keys(result2[0].inputs).length, 0);
|
||||
t.equal(Object.keys(result2[0].fields).length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with invalid xml', t => {
|
||||
const result = adapter(events.createbadxml);
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with empty field', t => {
|
||||
const result = adapter(events.createemptyfield);
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 3);
|
||||
t.end();
|
||||
});
|
||||
1040
scratch-vm/test/unit/engine_blocks.js
Normal file
1040
scratch-vm/test/unit/engine_blocks.js
Normal file
File diff suppressed because it is too large
Load Diff
26
scratch-vm/test/unit/engine_mutation-adapter.js
Normal file
26
scratch-vm/test/unit/engine_mutation-adapter.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const mutationAdapter = require('../../src/engine/mutation-adapter');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(mutationAdapter, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('convert DOM to Scratch object', t => {
|
||||
const testStringRaw = '"arbitrary" & \'complicated\' test string';
|
||||
const testStringEscaped = '\\"arbitrary\\" & 'complicated' test string';
|
||||
const xml = `<mutation blockInfo="{"text":"${testStringEscaped}"}"></mutation>`;
|
||||
const expectedMutation = {
|
||||
tagName: 'mutation',
|
||||
children: [],
|
||||
blockInfo: {
|
||||
text: testStringRaw
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: do we want to test passing a DOM node to `mutationAdapter`? Node.js doesn't have built-in DOM support...
|
||||
const mutationFromString = mutationAdapter(xml);
|
||||
t.deepEqual(mutationFromString, expectedMutation);
|
||||
t.end();
|
||||
});
|
||||
276
scratch-vm/test/unit/engine_runtime.js
Normal file
276
scratch-vm/test/unit/engine_runtime.js
Normal file
@@ -0,0 +1,276 @@
|
||||
const tap = require('tap');
|
||||
const path = require('path');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const MonitorRecord = require('../../src/engine/monitor-record');
|
||||
const {Map} = require('immutable');
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('spec', t => {
|
||||
const r = new Runtime();
|
||||
|
||||
t.type(Runtime, 'function');
|
||||
t.type(r, 'object');
|
||||
|
||||
// Test types of cloud data managing functions
|
||||
t.type(r.hasCloudData, 'function');
|
||||
t.type(r.canAddCloudVariable, 'function');
|
||||
t.type(r.addCloudVariable, 'function');
|
||||
t.type(r.removeCloudVariable, 'function');
|
||||
|
||||
t.ok(r instanceof Runtime);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('monitorStateEquals', t => {
|
||||
const r = new Runtime();
|
||||
const id = 'xklj4#!';
|
||||
const prevMonitorState = MonitorRecord({
|
||||
id,
|
||||
opcode: 'turtle whereabouts',
|
||||
value: '25'
|
||||
});
|
||||
const newMonitorDelta = Map({
|
||||
id,
|
||||
value: String(25)
|
||||
});
|
||||
r.requestAddMonitor(prevMonitorState);
|
||||
r.requestUpdateMonitor(newMonitorDelta);
|
||||
|
||||
t.equals(true, prevMonitorState === r._monitorState.get(id));
|
||||
t.equals(String(25), r._monitorState.get(id).get('value'));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('monitorStateDoesNotEqual', t => {
|
||||
const r = new Runtime();
|
||||
const id = 'xklj4#!';
|
||||
const params = {seven: 7};
|
||||
const prevMonitorState = MonitorRecord({
|
||||
id,
|
||||
opcode: 'turtle whereabouts',
|
||||
value: '25'
|
||||
});
|
||||
|
||||
// Value change
|
||||
let newMonitorDelta = Map({
|
||||
id,
|
||||
value: String(24)
|
||||
});
|
||||
r.requestAddMonitor(prevMonitorState);
|
||||
r.requestUpdateMonitor(newMonitorDelta);
|
||||
|
||||
t.equals(false, prevMonitorState.equals(r._monitorState.get(id)));
|
||||
t.equals(String(24), r._monitorState.get(id).get('value'));
|
||||
|
||||
// Prop change
|
||||
newMonitorDelta = Map({
|
||||
id: 'xklj4#!',
|
||||
params: params
|
||||
});
|
||||
r.requestUpdateMonitor(newMonitorDelta);
|
||||
|
||||
t.equals(false, prevMonitorState.equals(r._monitorState.get(id)));
|
||||
t.equals(String(24), r._monitorState.get(id).value);
|
||||
t.equals(params, r._monitorState.get(id).params);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getLabelForOpcode', t => {
|
||||
const r = new Runtime();
|
||||
|
||||
const fakeExtension = {
|
||||
id: 'fakeExtension',
|
||||
name: 'Fake Extension',
|
||||
blocks: [
|
||||
{
|
||||
info: {
|
||||
opcode: 'foo',
|
||||
json: {},
|
||||
text: 'Foo',
|
||||
xml: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
info: {
|
||||
opcode: 'foo_2',
|
||||
json: {},
|
||||
text: 'Foo 2',
|
||||
xml: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
r._blockInfo.push(fakeExtension);
|
||||
|
||||
const result1 = r.getLabelForOpcode('fakeExtension_foo');
|
||||
t.type(result1.category, 'string');
|
||||
t.type(result1.label, 'string');
|
||||
t.equals(result1.label, 'Fake Extension: Foo');
|
||||
|
||||
const result2 = r.getLabelForOpcode('fakeExtension_foo_2');
|
||||
t.type(result2.category, 'string');
|
||||
t.type(result2.label, 'string');
|
||||
t.equals(result2.label, 'Fake Extension: Foo 2');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Project loaded emits runtime event', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
let projectLoaded = false;
|
||||
|
||||
vm.runtime.addListener('PROJECT_LOADED', () => {
|
||||
projectLoaded = true;
|
||||
});
|
||||
|
||||
vm.loadProject(project).then(() => {
|
||||
t.equal(projectLoaded, true, 'Project load event emitted');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Cloud variable limit allows only 10 cloud variables', t => {
|
||||
// This is a test of just the cloud variable limit mechanism
|
||||
// The functions being tested below need to be used when
|
||||
// creating and deleting cloud variables in the runtime.
|
||||
|
||||
const rt = new Runtime();
|
||||
|
||||
t.equal(rt.hasCloudData(), false);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
t.equal(rt.canAddCloudVariable(), true);
|
||||
rt.addCloudVariable();
|
||||
// Adding a cloud variable should change the
|
||||
// result of the hasCloudData check
|
||||
t.equal(rt.hasCloudData(), true);
|
||||
}
|
||||
|
||||
|
||||
// We should be at the cloud variable limit now
|
||||
t.equal(rt.canAddCloudVariable(), false);
|
||||
|
||||
// Removing a cloud variable should allow the addition of exactly one more
|
||||
// when we are at the cloud variable limit
|
||||
rt.removeCloudVariable();
|
||||
|
||||
t.equal(rt.canAddCloudVariable(), true);
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.canAddCloudVariable(), false);
|
||||
|
||||
// Disposing of the runtime should reset the cloud variable limitations
|
||||
rt.dispose();
|
||||
t.equal(rt.hasCloudData(), false);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
t.equal(rt.canAddCloudVariable(), true);
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.hasCloudData(), true);
|
||||
}
|
||||
|
||||
// We should be at the cloud variable limit now
|
||||
t.equal(rt.canAddCloudVariable(), false);
|
||||
|
||||
t.end();
|
||||
|
||||
});
|
||||
|
||||
test('Starting the runtime emits an event', t => {
|
||||
let started = false;
|
||||
const rt = new Runtime();
|
||||
rt.addListener('RUNTIME_STARTED', () => {
|
||||
started = true;
|
||||
});
|
||||
rt.start();
|
||||
t.equal(started, true);
|
||||
rt.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Runtime cannot be started while already running', t => {
|
||||
const rt = new Runtime();
|
||||
rt.start(); // Start the first time
|
||||
|
||||
// Set up a flag/listener to check if it can be started again
|
||||
let started = false;
|
||||
rt.addListener('RUNTIME_STARTED', () => {
|
||||
started = true;
|
||||
});
|
||||
|
||||
// Starting again should not emit another event
|
||||
rt.start();
|
||||
t.equal(started, false);
|
||||
rt.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setCompatibilityMode restarts if it was already running', t => {
|
||||
const rt = new Runtime();
|
||||
rt.start(); // Start the first time
|
||||
|
||||
// Set up a flag/listener to check if it gets started again
|
||||
let started = false;
|
||||
rt.addListener('RUNTIME_STARTED', () => {
|
||||
started = true;
|
||||
});
|
||||
|
||||
rt.setCompatibilityMode(true);
|
||||
// TW: We make an intentional API change here. Changing compatibility mode won't emit a RUNTIME_STARTED
|
||||
// if the runtime is already running.
|
||||
t.equal(started, false);
|
||||
rt.quit();
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setCompatibilityMode does not restart if it was not running', t => {
|
||||
const rt = new Runtime();
|
||||
|
||||
let started = false;
|
||||
rt.addListener('RUNTIME_STARTED', () => {
|
||||
started = true;
|
||||
});
|
||||
|
||||
rt.setCompatibilityMode(true);
|
||||
t.equal(started, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Disposing the runtime emits an event', t => {
|
||||
let disposed = false;
|
||||
const rt = new Runtime();
|
||||
rt.addListener('RUNTIME_DISPOSED', () => {
|
||||
disposed = true;
|
||||
});
|
||||
rt.dispose();
|
||||
t.equal(disposed, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Clock is reset on runtime dispose', t => {
|
||||
const rt = new Runtime();
|
||||
const c = rt.ioDevices.clock;
|
||||
let simulatedTime = 0;
|
||||
|
||||
c._projectTimer = {
|
||||
timeElapsed: () => simulatedTime,
|
||||
start: () => {
|
||||
simulatedTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
t.ok(c.projectTimer() === 0);
|
||||
simulatedTime += 1000;
|
||||
t.ok(c.projectTimer() === 1);
|
||||
rt.dispose();
|
||||
// When the runtime is disposed, the clock should be reset
|
||||
t.ok(c.projectTimer() === 0);
|
||||
t.end();
|
||||
});
|
||||
255
scratch-vm/test/unit/engine_runtime_tw.js
Normal file
255
scratch-vm/test/unit/engine_runtime_tw.js
Normal file
@@ -0,0 +1,255 @@
|
||||
const tap = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const {Map} = require('immutable');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('setFramerate emits an event', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
rt.addListener('FRAMERATE_CHANGED', framerate => {
|
||||
if (framerate === 13) {
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
rt.setFramerate(13);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setFramerate and setCompatibilityMode do not emit a stop event if not running', t => {
|
||||
const rt = new Runtime();
|
||||
rt.addListener('RUNTIME_STOPPED', () => {
|
||||
t.fail();
|
||||
});
|
||||
rt.setFramerate(13);
|
||||
rt.setCompatibilityMode(true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setInterpolation emits an event', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
rt.addListener('INTERPOLATION_CHANGED', enabled => {
|
||||
if (enabled) {
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
rt.setInterpolation(true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setInterpolation does not restart runtime if not running', t => {
|
||||
const rt = new Runtime();
|
||||
let started = false;
|
||||
let stopped = false;
|
||||
rt.addListener('RUNTIME_STARTED', () => {
|
||||
started = true;
|
||||
});
|
||||
rt.addListener('RUNTIME_STOPPED', () => {
|
||||
stopped = true;
|
||||
});
|
||||
rt.setInterpolation(true);
|
||||
t.equal(started, false);
|
||||
t.equal(stopped, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Stopping the runtime emits an event', t => {
|
||||
const rt = new Runtime();
|
||||
rt.start();
|
||||
let stopped = false;
|
||||
rt.addListener('RUNTIME_STOPPED', () => {
|
||||
stopped = true;
|
||||
});
|
||||
rt.stop();
|
||||
t.equal(stopped, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Stop does not emit an event if already stopped', t => {
|
||||
const rt = new Runtime();
|
||||
let stopped = false;
|
||||
rt.addListener('RUNTIME_STOPPED', () => {
|
||||
stopped = true;
|
||||
});
|
||||
rt.stop();
|
||||
t.equal(stopped, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setRuntimeOptions emits an event', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
rt.addListener('RUNTIME_OPTIONS_CHANGED', options => {
|
||||
if (options.option === 17) {
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
rt.setRuntimeOptions({option: 17});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setRuntimeOptions supports partial updates', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
rt.setRuntimeOptions({option: 17});
|
||||
rt.addListener('RUNTIME_OPTIONS_CHANGED', options => {
|
||||
if (options.option === 17) {
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
rt.setRuntimeOptions({otherOption: 1});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setCompilerOptions emits an event', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
rt.addListener('COMPILER_OPTIONS_CHANGED', options => {
|
||||
if (options.option === 17) {
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
rt.setCompilerOptions({option: 17});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setCompilerOptions supports partial updates', t => {
|
||||
t.plan(1);
|
||||
const rt = new Runtime();
|
||||
rt.setCompilerOptions({option: 17});
|
||||
rt.addListener('COMPILER_OPTIONS_CHANGED', options => {
|
||||
if (options.option === 17) {
|
||||
t.pass();
|
||||
}
|
||||
});
|
||||
rt.setCompilerOptions({otherOption: 1});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('maxClones runtime option', t => {
|
||||
const rt = new Runtime();
|
||||
rt.setRuntimeOptions({maxClones: 10});
|
||||
for (let i = 0; i < 10; i++) {
|
||||
t.equal(rt.clonesAvailable(), true);
|
||||
rt.changeCloneCounter(1);
|
||||
}
|
||||
rt.changeCloneCounter(1);
|
||||
t.equal(rt.clonesAvailable(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stageWidth and stageHeight', t => {
|
||||
const rt = new Runtime();
|
||||
t.equal(rt.stageWidth, 480);
|
||||
t.equal(rt.stageHeight, 360);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('debug', t => {
|
||||
const rt = new Runtime();
|
||||
t.equal(rt.debug, false);
|
||||
rt.enableDebug();
|
||||
t.equal(rt.debug, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setStageSize preserves monitor position relative to center of stage', t => {
|
||||
const rt = new Runtime();
|
||||
rt.requestAddMonitor(new Map([
|
||||
['id', 'abc'],
|
||||
// top right corner
|
||||
['x', 0],
|
||||
['y', 0]
|
||||
]));
|
||||
rt.setStageSize(640, 362);
|
||||
const finalState = rt.getMonitorState().get('abc');
|
||||
t.equal(finalState.get('x'), 80);
|
||||
t.equal(finalState.get('y'), 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setStageSize argument range', t => {
|
||||
t.plan(6);
|
||||
const rt = new Runtime();
|
||||
|
||||
rt.once('STAGE_SIZE_CHANGED', (width, height) => {
|
||||
t.equal(width, 101);
|
||||
t.equal(height, 103);
|
||||
});
|
||||
rt.setStageSize(101, 103);
|
||||
|
||||
rt.once('STAGE_SIZE_CHANGED', (width, height) => {
|
||||
t.equal(width, 1);
|
||||
t.equal(height, 1);
|
||||
});
|
||||
rt.setStageSize(-3.1, 0);
|
||||
|
||||
rt.once('STAGE_SIZE_CHANGED', (width, height) => {
|
||||
t.equal(width, 99);
|
||||
t.equal(height, 10000);
|
||||
});
|
||||
rt.setStageSize(99.3, 10000);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('STAGE_SIZE_CHANGED does not fire if no change', t => {
|
||||
const rt = new Runtime();
|
||||
rt.on('STAGE_SIZE_CHANGED', () => {
|
||||
t.fail('STAGE_SIZE_CHANGED emitted');
|
||||
});
|
||||
rt.setStageSize(rt.stageWidth, rt.stageHeight);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getNumberOfCloudVariables', t => {
|
||||
const rt = new Runtime();
|
||||
|
||||
t.equal(rt.getNumberOfCloudVariables(), 0);
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.getNumberOfCloudVariables(), 1);
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.getNumberOfCloudVariables(), 2);
|
||||
rt.removeCloudVariable();
|
||||
t.equal(rt.getNumberOfCloudVariables(), 1);
|
||||
rt.removeCloudVariable();
|
||||
t.equal(rt.getNumberOfCloudVariables(), 0);
|
||||
|
||||
rt.dispose();
|
||||
t.equal(rt.getNumberOfCloudVariables(), 0);
|
||||
rt.addCloudVariable();
|
||||
t.equal(rt.getNumberOfCloudVariables(), 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('currentStepTime default value', t => {
|
||||
const rt = new Runtime();
|
||||
t.type(rt.currentStepTime, 'number');
|
||||
t.ok(rt.currentStepTime > 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('convertToPackagedRuntime', t => {
|
||||
const rt = new Runtime();
|
||||
t.equal(rt.isPackaged, false);
|
||||
rt.convertToPackagedRuntime();
|
||||
t.equal(rt.isPackaged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('convertToPackagedRuntime and attachStorage call order', t => {
|
||||
try {
|
||||
const rt1 = new Runtime();
|
||||
rt1.attachStorage(makeTestStorage());
|
||||
rt1.convertToPackagedRuntime();
|
||||
} catch (e) {
|
||||
t.equal(e.message, 'convertToPackagedRuntime must be called before attachStorage');
|
||||
}
|
||||
const rt2 = new Runtime();
|
||||
rt2.convertToPackagedRuntime();
|
||||
rt2.attachStorage(makeTestStorage());
|
||||
t.end();
|
||||
});
|
||||
193
scratch-vm/test/unit/engine_sequencer.js
Normal file
193
scratch-vm/test/unit/engine_sequencer.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const test = require('tap').test;
|
||||
const Sequencer = require('../../src/engine/sequencer');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(Sequencer, 'function');
|
||||
|
||||
const r = new Runtime();
|
||||
const s = new Sequencer(r);
|
||||
|
||||
t.type(s, 'object');
|
||||
t.ok(s instanceof Sequencer);
|
||||
|
||||
t.type(s.stepThreads, 'function');
|
||||
t.type(s.stepThread, 'function');
|
||||
t.type(s.stepToBranch, 'function');
|
||||
t.type(s.stepToProcedure, 'function');
|
||||
t.type(s.retireThread, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
const randomString = function () {
|
||||
const top = Math.random().toString(36);
|
||||
return top.substring(7);
|
||||
};
|
||||
|
||||
const generateBlock = function (id) {
|
||||
const block = {fields: Object,
|
||||
id: id,
|
||||
inputs: {},
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'fakeName',
|
||||
next: null,
|
||||
opcode: 'procedures_definition',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
return block;
|
||||
};
|
||||
|
||||
const generateBlockInput = function (id, next, inp) {
|
||||
const block = {fields: Object,
|
||||
id: id,
|
||||
inputs: {SUBSTACK: {block: inp, name: 'SUBSTACK'}},
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'fakeName',
|
||||
next: next,
|
||||
opcode: 'procedures_definition',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
return block;
|
||||
};
|
||||
|
||||
const generateThread = function (runtime) {
|
||||
const s = new Sprite(null, runtime);
|
||||
const rt = new RenderedTarget(s, runtime);
|
||||
const th = new Thread(randomString());
|
||||
|
||||
let next = randomString();
|
||||
let inp = randomString();
|
||||
let name = th.topBlock;
|
||||
|
||||
const pushStack = id => {
|
||||
th.pushStack(id);
|
||||
th.peekStackFrame().op = {id};
|
||||
};
|
||||
|
||||
rt.blocks.createBlock(generateBlockInput(name, next, inp));
|
||||
pushStack(name);
|
||||
rt.blocks.createBlock(generateBlock(inp));
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
name = next;
|
||||
next = randomString();
|
||||
inp = randomString();
|
||||
|
||||
rt.blocks.createBlock(generateBlockInput(name, next, inp));
|
||||
pushStack(name);
|
||||
rt.blocks.createBlock(generateBlock(inp));
|
||||
}
|
||||
rt.blocks.createBlock(generateBlock(next));
|
||||
pushStack(next);
|
||||
th.target = rt;
|
||||
th.blockContainer = rt.blocks;
|
||||
|
||||
runtime.threads.push(th);
|
||||
|
||||
return th;
|
||||
};
|
||||
|
||||
test('stepThread', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sequencer(r);
|
||||
let th = generateThread(r);
|
||||
t.notEquals(th.status, Thread.STATUS_DONE);
|
||||
s.stepThread(th);
|
||||
t.strictEquals(th.status, Thread.STATUS_DONE);
|
||||
th = generateThread(r);
|
||||
th.status = Thread.STATUS_YIELD;
|
||||
s.stepThread(th);
|
||||
t.notEquals(th.status, Thread.STATUS_DONE);
|
||||
th.status = Thread.STATUS_PROMISE_WAIT;
|
||||
s.stepThread(th);
|
||||
t.notEquals(th.status, Thread.STATUS_DONE);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stepToBranch', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sequencer(r);
|
||||
const th = generateThread(r);
|
||||
s.stepToBranch(th, 2, false);
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
th.popStack();
|
||||
s.stepToBranch(th, 1, false);
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
th.popStack();
|
||||
th.popStack();
|
||||
s.stepToBranch(th, 1, false);
|
||||
t.notEquals(th.peekStack(), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('retireThread', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sequencer(r);
|
||||
const th = generateThread(r);
|
||||
t.strictEquals(th.stack.length, 12);
|
||||
s.retireThread(th);
|
||||
t.strictEquals(th.stack.length, 0);
|
||||
t.strictEquals(th.status, Thread.STATUS_DONE);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stepToProcedure', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sequencer(r);
|
||||
const th = generateThread(r);
|
||||
let expectedBlock = th.peekStack();
|
||||
s.stepToProcedure(th, '');
|
||||
t.strictEquals(th.peekStack(), expectedBlock);
|
||||
s.stepToProcedure(th, 'faceCode');
|
||||
t.strictEquals(th.peekStack(), expectedBlock);
|
||||
|
||||
th.target.blocks.createBlock({
|
||||
id: 'internalId',
|
||||
opcode: 'procedures_prototype',
|
||||
mutation: {
|
||||
proccode: 'othercode'
|
||||
}
|
||||
});
|
||||
expectedBlock = th.stack[th.stack.length - 4];
|
||||
th.target.blocks.getBlock(expectedBlock).inputs.custom_block = {
|
||||
type: 'custom_block',
|
||||
block: 'internalId'
|
||||
};
|
||||
s.stepToProcedure(th, 'othercode');
|
||||
t.strictEquals(th.peekStack(), expectedBlock);
|
||||
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stepThreads', t => {
|
||||
const r = new Runtime();
|
||||
r.currentStepTime = Infinity;
|
||||
const s = new Sequencer(r);
|
||||
t.strictEquals(s.stepThreads().length, 0);
|
||||
generateThread(r);
|
||||
t.strictEquals(r.threads.length, 1);
|
||||
// Threads should be marked DONE and removed in the same step they finish.
|
||||
t.strictEquals(s.stepThreads().length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
813
scratch-vm/test/unit/engine_target.js
Normal file
813
scratch-vm/test/unit/engine_target.js
Normal file
@@ -0,0 +1,813 @@
|
||||
const test = require('tap').test;
|
||||
const Target = require('../../src/engine/target');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
const adapter = require('../../src/engine/adapter');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const events = require('../fixtures/events.json');
|
||||
|
||||
test('spec', t => {
|
||||
const target = new Target(new Runtime());
|
||||
|
||||
t.type(Target, 'function');
|
||||
t.type(target, 'object');
|
||||
t.ok(target instanceof Target);
|
||||
|
||||
t.type(target.id, 'string');
|
||||
t.type(target.blocks, 'object');
|
||||
t.type(target.variables, 'object');
|
||||
t.type(target.comments, 'object');
|
||||
t.type(target._customState, 'object');
|
||||
|
||||
t.type(target.createVariable, 'function');
|
||||
t.type(target.renameVariable, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Create Variable tests.
|
||||
test('createVariable', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar');
|
||||
t.equal(variable.type, Variable.SCALAR_TYPE);
|
||||
t.equal(variable.value, 0);
|
||||
t.equal(variable.isCloud, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Create Same Variable twice.
|
||||
test('createVariable2', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Create a list
|
||||
test('createListVariable creates a list', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('foo', 'bar', Variable.LIST_TYPE);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar');
|
||||
t.equal(variable.type, Variable.LIST_TYPE);
|
||||
t.assert(variable.value instanceof Array, true);
|
||||
t.equal(variable.value.length, 0);
|
||||
t.equal(variable.isCloud, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('createVariable calls cloud io device\'s requestCreateVariable', t => {
|
||||
const runtime = new Runtime();
|
||||
// Mock the requestCreateVariable function
|
||||
let requestCreateCloudWasCalled = false;
|
||||
runtime.ioDevices.cloud.requestCreateVariable = () => {
|
||||
requestCreateCloudWasCalled = true;
|
||||
};
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = true;
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE, true /* isCloud */);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar');
|
||||
t.equal(variable.type, Variable.SCALAR_TYPE);
|
||||
t.equal(variable.value, 0);
|
||||
t.equal(variable.isCloud, true);
|
||||
t.equal(requestCreateCloudWasCalled, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('createVariable does not call cloud io device\'s requestCreateVariable if target is not stage', t => {
|
||||
const runtime = new Runtime();
|
||||
// Mock the requestCreateVariable function
|
||||
let requestCreateCloudWasCalled = false;
|
||||
runtime.ioDevices.cloud.requestCreateVariable = () => {
|
||||
requestCreateCloudWasCalled = true;
|
||||
};
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = false;
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE, true /* isCloud */);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar');
|
||||
t.equal(variable.type, Variable.SCALAR_TYPE);
|
||||
t.equal(variable.value, 0);
|
||||
// isCloud flag doesn't get set if the target is not the stage
|
||||
t.equal(variable.isCloud, false);
|
||||
t.equal(requestCreateCloudWasCalled, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('createVariable throws when given invalid type', t => {
|
||||
const target = new Target(new Runtime());
|
||||
t.throws(
|
||||
(() => target.createVariable('foo', 'bar', 'baz')),
|
||||
new Error('Invalid variable type: baz')
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Rename Variable tests.
|
||||
test('renameVariable', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar2');
|
||||
t.equal(variable.value, 0);
|
||||
t.equal(variable.isCloud, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Rename Variable that doesn't exist.
|
||||
test('renameVariable2', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Rename Variable that with id that exists as another variable's name.
|
||||
// Expect no change.
|
||||
test('renameVariable3', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('foo1', 'foo', Variable.SCALAR_TYPE);
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo1');
|
||||
t.equal(variable.name, 'foo');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('renameVariable calls cloud io device\'s requestRenameVariable function', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
let requestRenameVariableWasCalled = false;
|
||||
runtime.ioDevices.cloud.requestRenameVariable = () => {
|
||||
requestRenameVariableWasCalled = true;
|
||||
};
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = true;
|
||||
const mockCloudVar = new Variable('foo', 'bar', Variable.SCALAR_TYPE, true);
|
||||
target.variables[mockCloudVar.id] = mockCloudVar;
|
||||
runtime.addTarget(target);
|
||||
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar2');
|
||||
t.equal(variable.value, 0);
|
||||
t.equal(variable.isCloud, true);
|
||||
t.equal(requestRenameVariableWasCalled, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('renameVariable does not call cloud io device\'s requestRenameVariable function if target is not stage', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
let requestRenameVariableWasCalled = false;
|
||||
runtime.ioDevices.cloud.requestRenameVariable = () => {
|
||||
requestRenameVariableWasCalled = true;
|
||||
};
|
||||
|
||||
const target = new Target(runtime);
|
||||
const mockCloudVar = new Variable('foo', 'bar', Variable.SCALAR_TYPE, true);
|
||||
target.variables[mockCloudVar.id] = mockCloudVar;
|
||||
runtime.addTarget(target);
|
||||
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar2');
|
||||
t.equal(variable.value, 0);
|
||||
t.equal(variable.isCloud, true);
|
||||
t.equal(requestRenameVariableWasCalled, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Delete Variable tests.
|
||||
test('deleteVariable', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
target.deleteVariable('foo');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Delete Variable that doesn't exist.
|
||||
test('deleteVariable2', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.deleteVariable('foo');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteVariable calls cloud io device\'s requestRenameVariable function', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
let requestDeleteVariableWasCalled = false;
|
||||
runtime.ioDevices.cloud.requestDeleteVariable = () => {
|
||||
requestDeleteVariableWasCalled = true;
|
||||
};
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = true;
|
||||
const mockCloudVar = new Variable('foo', 'bar', Variable.SCALAR_TYPE, true);
|
||||
target.variables[mockCloudVar.id] = mockCloudVar;
|
||||
runtime.addTarget(target);
|
||||
|
||||
target.deleteVariable('foo');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
t.equal(requestDeleteVariableWasCalled, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteVariable calls cloud io device\'s requestRenameVariable function', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
let requestDeleteVariableWasCalled = false;
|
||||
runtime.ioDevices.cloud.requestDeleteVariable = () => {
|
||||
requestDeleteVariableWasCalled = true;
|
||||
};
|
||||
|
||||
const target = new Target(runtime);
|
||||
const mockCloudVar = new Variable('foo', 'bar', Variable.SCALAR_TYPE, true);
|
||||
target.variables[mockCloudVar.id] = mockCloudVar;
|
||||
runtime.addTarget(target);
|
||||
|
||||
target.deleteVariable('foo');
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
t.equal(requestDeleteVariableWasCalled, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateVariable creates a new variable with a new ID by default', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('a var ID', 'foo', Variable.SCALAR_TYPE);
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
const originalVariable = target.variables['a var ID'];
|
||||
originalVariable.value = 10;
|
||||
const newVariable = target.duplicateVariable('a var ID');
|
||||
// Duplicating a variable should not add the variable to the current target
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
// Duplicate variable should have a different ID from the original unless specified to keep the original ID.
|
||||
t.notEqual(newVariable.id, 'a var ID');
|
||||
t.type(target.variables[newVariable.id], 'undefined');
|
||||
|
||||
// Duplicate variable should start out with the same value as the original variable
|
||||
t.equal(newVariable.value, originalVariable.value);
|
||||
|
||||
// Modifying one variable should not modify the other
|
||||
newVariable.value = 15;
|
||||
t.notEqual(newVariable.value, originalVariable.value);
|
||||
t.equal(originalVariable.value, 10);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateVariable creates new array reference for list variable.value', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const arr = [1, 2, 3];
|
||||
target.createVariable('a var ID', 'arr', Variable.LIST_TYPE);
|
||||
const originalVariable = target.variables['a var ID'];
|
||||
originalVariable.value = arr;
|
||||
const newVariable = target.duplicateVariable('a var ID');
|
||||
// Values are deeply equal but not the same object
|
||||
t.deepEqual(originalVariable.value, newVariable.value);
|
||||
t.notEqual(originalVariable.value, newVariable.value);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateVariable creates a new variable with a original ID if specified', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('a var ID', 'foo', Variable.SCALAR_TYPE);
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
const originalVariable = target.variables['a var ID'];
|
||||
originalVariable.value = 10;
|
||||
const newVariable = target.duplicateVariable('a var ID', true);
|
||||
// Duplicating a variable should not add the variable to the current target
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
// Duplicate variable should have the same ID as the original when specified
|
||||
t.equal(newVariable.id, 'a var ID');
|
||||
|
||||
// Duplicate variable should start out with the same value as the original variable
|
||||
t.equal(newVariable.value, originalVariable.value);
|
||||
|
||||
// Modifying one variable should not modify the other
|
||||
newVariable.value = 15;
|
||||
t.notEqual(newVariable.value, originalVariable.value);
|
||||
t.equal(originalVariable.value, 10);
|
||||
// The target should still have the original variable with the original value
|
||||
t.equal(target.variables['a var ID'].value, 10);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateVariable returns null if variable with specified ID does not exist', t => {
|
||||
const target = new Target(new Runtime());
|
||||
|
||||
const variable = target.duplicateVariable('a var ID');
|
||||
t.equal(variable, null);
|
||||
t.equal(Object.keys(target.variables).length, 0);
|
||||
|
||||
target.createVariable('var id', 'foo', Variable.SCALAR_TYPE);
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
|
||||
const anotherVariable = target.duplicateVariable('another var ID');
|
||||
t.equal(anotherVariable, null);
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.type(target.variables['another var ID'], 'undefined');
|
||||
t.type(target.variables['var id'], 'object');
|
||||
t.notEqual(target.variables['var id'], null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateVariables duplicates all variables', t => {
|
||||
const target = new Target(new Runtime());
|
||||
target.createVariable('var ID 1', 'var1', Variable.SCALAR_TYPE);
|
||||
target.createVariable('var ID 2', 'var2', Variable.SCALAR_TYPE);
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 2);
|
||||
|
||||
const var1 = target.variables['var ID 1'];
|
||||
const var2 = target.variables['var ID 2'];
|
||||
|
||||
var1.value = 3;
|
||||
var2.value = 'foo';
|
||||
|
||||
const duplicateVariables = target.duplicateVariables();
|
||||
|
||||
// Duplicating a target's variables should not change the target's own variables.
|
||||
t.equal(Object.keys(target.variables).length, 2);
|
||||
t.equal(Object.keys(duplicateVariables).length, 2);
|
||||
|
||||
// Should be able to find original var IDs in both this target's variables and
|
||||
// the duplicate variables since a blocks container was not specified.
|
||||
t.equal(Object.prototype.hasOwnProperty.call(target.variables, 'var ID 1'), true);
|
||||
t.equal(Object.prototype.hasOwnProperty.call(target.variables, 'var ID 2'), true);
|
||||
t.equal(Object.prototype.hasOwnProperty.call(duplicateVariables, 'var ID 1'), true);
|
||||
t.equal(Object.prototype.hasOwnProperty.call(duplicateVariables, 'var ID 1'), true);
|
||||
|
||||
// Values of the duplicate varaiables should match the value of the original values at the time of duplication
|
||||
t.equal(target.variables['var ID 1'].value, duplicateVariables['var ID 1'].value);
|
||||
t.equal(duplicateVariables['var ID 1'].value, 3);
|
||||
t.equal(target.variables['var ID 2'].value, duplicateVariables['var ID 2'].value);
|
||||
t.equal(duplicateVariables['var ID 2'].value, 'foo');
|
||||
|
||||
// The two sets of variables should still be distinct, modifying the target's variables
|
||||
// should not affect the duplicated variables, and vice-versa
|
||||
|
||||
var1.value = 10;
|
||||
t.equal(target.variables['var ID 1'].value, 10);
|
||||
t.equal(duplicateVariables['var ID 1'].value, 3); // should remain unchanged from initial value
|
||||
|
||||
duplicateVariables['var ID 2'].value = 'bar';
|
||||
t.equal(target.variables['var ID 2'].value, 'foo');
|
||||
|
||||
// Deleting a variable on the target should not change the duplicated variables
|
||||
target.deleteVariable('var ID 1');
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(duplicateVariables).length, 2);
|
||||
t.type(duplicateVariables['var ID 1'], 'object');
|
||||
t.notEqual(duplicateVariables['var ID 1'], null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateVariables re-IDs variables when a block container is provided', t => {
|
||||
const target = new Target(new Runtime());
|
||||
|
||||
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
target.createVariable('another var id', 'var2', Variable.SCALAR_TYPE);
|
||||
|
||||
// Create a block on the target which references the variable with id 'mock var id'
|
||||
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.value, 'a mock variable');
|
||||
|
||||
// Deep clone this target's blocks to pass in to 'duplicateVariables'
|
||||
const copiedBlocks = target.blocks.duplicate();
|
||||
|
||||
// The copied block should still have the same ID, and its VARIABLE field should still refer to
|
||||
// the original variable id
|
||||
t.type(copiedBlocks.getBlock('a block'), 'object');
|
||||
t.type(copiedBlocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(copiedBlocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(copiedBlocks.getBlock('a block').fields.VARIABLE.value, 'a mock variable');
|
||||
|
||||
const duplicateVariables = target.duplicateVariables(copiedBlocks);
|
||||
|
||||
// Duplicate variables should have new IDs
|
||||
t.equal(Object.keys(duplicateVariables).length, 2);
|
||||
t.type(duplicateVariables['mock var id'], 'undefined');
|
||||
t.type(duplicateVariables['another var id'], 'undefined');
|
||||
|
||||
// Duplicate variables still have the same names..
|
||||
const dupes = Object.values(duplicateVariables);
|
||||
const dupeVarNames = dupes.map(v => v.name);
|
||||
|
||||
t.notEqual(dupeVarNames.indexOf('a mock variable'), -1);
|
||||
t.notEqual(dupeVarNames.indexOf('var2'), -1);
|
||||
|
||||
// Duplicating variables should not change blocks on current target
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.value, 'a mock variable');
|
||||
|
||||
// The copied blocks passed into duplicateVariables should now reference the new
|
||||
// variable ID
|
||||
const mockVariableDupe = dupes[dupeVarNames.indexOf('a mock variable')];
|
||||
const mockVarDupeID = mockVariableDupe.id;
|
||||
|
||||
t.type(copiedBlocks.getBlock('a block'), 'object');
|
||||
t.equal(copiedBlocks.getBlock('a block').fields.VARIABLE.id, mockVarDupeID);
|
||||
t.equal(copiedBlocks.getBlock('a block').fields.VARIABLE.value, 'a mock variable');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lookupOrCreateList creates a list if var with given id or var with given name does not exist', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const variables = target.variables;
|
||||
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
const listVar = target.lookupOrCreateList('foo', 'bar');
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
t.equal(listVar.id, 'foo');
|
||||
t.equal(listVar.name, 'bar');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lookupOrCreateList returns list if one with given id exists', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const variables = target.variables;
|
||||
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
target.createVariable('foo', 'bar', Variable.LIST_TYPE);
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
|
||||
const listVar = target.lookupOrCreateList('foo', 'bar');
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
t.equal(listVar.id, 'foo');
|
||||
t.equal(listVar.name, 'bar');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lookupOrCreateList succeeds in finding list if id is incorrect but name matches', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const variables = target.variables;
|
||||
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
target.createVariable('foo', 'bar', Variable.LIST_TYPE);
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
|
||||
const listVar = target.lookupOrCreateList('not foo', 'bar');
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
t.equal(listVar.id, 'foo');
|
||||
t.equal(listVar.name, 'bar');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lookupBroadcastMsg returns the var with given id if exists', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const variables = target.variables;
|
||||
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
target.createVariable('foo', 'bar', Variable.BROADCAST_MESSAGE_TYPE);
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
|
||||
const broadcastMsg = target.lookupBroadcastMsg('foo', 'bar');
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
t.equal(broadcastMsg.id, 'foo');
|
||||
t.equal(broadcastMsg.name, 'bar');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('createComment adds a comment to the target', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const comments = target.comments;
|
||||
|
||||
t.equal(Object.keys(comments).length, 0);
|
||||
target.createComment('a comment', null, 'some comment text',
|
||||
10, 20, 200, 300, true);
|
||||
t.equal(Object.keys(comments).length, 1);
|
||||
|
||||
const comment = comments['a comment'];
|
||||
t.notEqual(comment, null);
|
||||
t.equal(comment.blockId, null);
|
||||
t.equal(comment.text, 'some comment text');
|
||||
t.equal(comment.x, 10);
|
||||
t.equal(comment.y, 20);
|
||||
t.equal(comment.width, 200);
|
||||
t.equal(comment.height, 300);
|
||||
t.equal(comment.minimized, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('creating comment with id that already exists does not change existing comment', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const comments = target.comments;
|
||||
|
||||
t.equal(Object.keys(comments).length, 0);
|
||||
target.createComment('a comment', null, 'some comment text',
|
||||
10, 20, 200, 300, true);
|
||||
t.equal(Object.keys(comments).length, 1);
|
||||
|
||||
target.createComment('a comment', null,
|
||||
'some new comment text', 40, 50, 300, 400, false);
|
||||
|
||||
const comment = comments['a comment'];
|
||||
t.notEqual(comment, null);
|
||||
// All of the comment properties should remain unchanged from the first
|
||||
// time createComment was called
|
||||
t.equal(comment.blockId, null);
|
||||
t.equal(comment.text, 'some comment text');
|
||||
t.equal(comment.x, 10);
|
||||
t.equal(comment.y, 20);
|
||||
t.equal(comment.width, 200);
|
||||
t.equal(comment.height, 300);
|
||||
t.equal(comment.minimized, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('creating a comment with a blockId also updates the comment property on the block', t => {
|
||||
const target = new Target(new Runtime());
|
||||
const comments = target.comments;
|
||||
// Create a mock block on the target
|
||||
target.blocks = {
|
||||
'a mock block': {
|
||||
id: 'a mock block'
|
||||
}
|
||||
};
|
||||
|
||||
// Mock the getBlock function that's used in commentCreate
|
||||
target.blocks.getBlock = id => target.blocks[id];
|
||||
|
||||
t.equal(Object.keys(comments).length, 0);
|
||||
target.createComment('a comment', 'a mock block', 'some comment text',
|
||||
10, 20, 200, 300, true);
|
||||
t.equal(Object.keys(comments).length, 1);
|
||||
|
||||
const comment = comments['a comment'];
|
||||
t.equal(comment.blockId, 'a mock block');
|
||||
t.equal(target.blocks.getBlock('a mock block').comment, 'a comment');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fixUpVariableReferences fixes sprite global var conflicting with project global var', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const stage = new Target(runtime);
|
||||
stage.isStage = true;
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = false;
|
||||
|
||||
runtime.targets = [stage, target];
|
||||
|
||||
// Create a global variable
|
||||
stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
|
||||
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 0);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
|
||||
target.fixUpVariableReferences();
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 0);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'pre-existing global var id');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fixUpVariableReferences fixes sprite local var conflicting with project global var', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const stage = new Target(runtime);
|
||||
stage.isStage = true;
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = false;
|
||||
target.getName = () => 'Target';
|
||||
|
||||
runtime.targets = [stage, target];
|
||||
|
||||
// Create a global variable
|
||||
stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
|
||||
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||
|
||||
target.fixUpVariableReferences();
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(target.variables['mock var id'].name, 'Target: a mock variable');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fixUpVariableReferences fixes conflicting sprite local var without blocks referencing var', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const stage = new Target(runtime);
|
||||
stage.isStage = true;
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = false;
|
||||
target.getName = () => 'Target';
|
||||
|
||||
runtime.targets = [stage, target];
|
||||
|
||||
// Create a global variable
|
||||
stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||
|
||||
target.fixUpVariableReferences();
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.equal(target.variables['mock var id'].name, 'Target: a mock variable');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fixUpVariableReferences fixes sprite global var conflicting with other sprite\'s local var', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const stage = new Target(runtime);
|
||||
stage.isStage = true;
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = false;
|
||||
|
||||
const existingTarget = new Target(runtime);
|
||||
existingTarget.isStage = false;
|
||||
|
||||
runtime.targets = [stage, target, existingTarget];
|
||||
|
||||
// Create a local variable on the pre-existing target
|
||||
existingTarget.createVariable('pre-existing local var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
|
||||
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||
|
||||
t.equal(Object.keys(existingTarget.variables).length, 1);
|
||||
const existingVariable = Object.values(existingTarget.variables)[0];
|
||||
t.equal(existingVariable.name, 'a mock variable');
|
||||
t.equal(Object.keys(target.variables).length, 0);
|
||||
t.equal(Object.keys(stage.variables).length, 0);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
|
||||
target.fixUpVariableReferences();
|
||||
|
||||
t.equal(Object.keys(existingTarget.variables).length, 1);
|
||||
t.equal(existingVariable.name, 'a mock variable');
|
||||
t.equal(Object.keys(target.variables).length, 0);
|
||||
t.equal(Object.keys(stage.variables).length, 1);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
const newGlobal = stage.variables[Object.keys(stage.variables)[0]];
|
||||
t.equal(newGlobal.name, 'a mock variable2');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fixUpVariableReferences does not change variable name if there is no variable conflict', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
const stage = new Target(runtime);
|
||||
stage.isStage = true;
|
||||
|
||||
const target = new Target(runtime);
|
||||
target.isStage = false;
|
||||
target.getName = () => 'Target';
|
||||
|
||||
runtime.targets = [stage, target];
|
||||
|
||||
// Create a global variable
|
||||
stage.createVariable('pre-existing global var id', 'a variable', Variable.SCALAR_TYPE);
|
||||
stage.createVariable('pre-existing global list id', 'a mock variable', Variable.LIST_TYPE);
|
||||
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||
|
||||
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(stage.variables).length, 2);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||
|
||||
target.fixUpVariableReferences();
|
||||
|
||||
t.equal(Object.keys(target.variables).length, 1);
|
||||
t.equal(Object.keys(stage.variables).length, 2);
|
||||
t.type(target.blocks.getBlock('a block'), 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||
|
||||
t.end();
|
||||
});
|
||||
290
scratch-vm/test/unit/engine_thread.js
Normal file
290
scratch-vm/test/unit/engine_thread.js
Normal file
@@ -0,0 +1,290 @@
|
||||
const test = require('tap').test;
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(Thread, 'function');
|
||||
|
||||
const th = new Thread('arbitraryString');
|
||||
t.type(th, 'object');
|
||||
t.ok(th instanceof Thread);
|
||||
t.type(th.pushStack, 'function');
|
||||
t.type(th.reuseStackForNextBlock, 'function');
|
||||
t.type(th.popStack, 'function');
|
||||
t.type(th.stopThisScript, 'function');
|
||||
t.type(th.peekStack, 'function');
|
||||
t.type(th.peekStackFrame, 'function');
|
||||
t.type(th.peekParentStackFrame, 'function');
|
||||
t.type(th.pushReportedValue, 'function');
|
||||
t.type(th.initParams, 'function');
|
||||
t.type(th.pushParam, 'function');
|
||||
t.type(th.peekStack, 'function');
|
||||
t.type(th.getParam, 'function');
|
||||
t.type(th.atStackTop, 'function');
|
||||
t.type(th.goToNextBlock, 'function');
|
||||
t.type(th.isRecursiveCall, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('pushStack', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('popStack', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
t.strictEquals(th.popStack(), 'arbitraryString');
|
||||
t.strictEquals(th.popStack(), undefined);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('atStackTop', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
th.pushStack('secondString');
|
||||
t.strictEquals(th.atStackTop(), false);
|
||||
th.popStack();
|
||||
t.strictEquals(th.atStackTop(), true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('reuseStackForNextBlock', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
th.reuseStackForNextBlock('secondString');
|
||||
t.strictEquals(th.popStack(), 'secondString');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('peekStackFrame', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
t.strictEquals(th.peekStackFrame().warpMode, false);
|
||||
th.popStack();
|
||||
t.strictEquals(th.peekStackFrame(), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('peekParentStackFrame', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
th.peekStackFrame().warpMode = true;
|
||||
t.strictEquals(th.peekParentStackFrame(), null);
|
||||
th.pushStack('secondString');
|
||||
t.strictEquals(th.peekParentStackFrame().warpMode, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('pushReportedValue', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
th.pushStack('secondString');
|
||||
th.pushReportedValue('value');
|
||||
t.strictEquals(th.justReported, 'value');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('peekStack', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
t.strictEquals(th.peekStack(), 'arbitraryString');
|
||||
th.popStack();
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('PushGetParam', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
th.pushStack('arbitraryString');
|
||||
th.initParams();
|
||||
th.pushParam('testParam', 'testValue');
|
||||
t.strictEquals(th.peekStackFrame().params.testParam, 'testValue');
|
||||
t.strictEquals(th.getParam('testParam'), 'testValue');
|
||||
// Params outside of define stack always evaluate to null
|
||||
t.strictEquals(th.getParam('nonExistentParam'), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('goToNextBlock', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const rt = new RenderedTarget(s, r);
|
||||
const block1 = {fields: Object,
|
||||
id: 'arbitraryString',
|
||||
inputs: Object,
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: 'secondString',
|
||||
opcode: 'motion_movesteps',
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
const block2 = {fields: Object,
|
||||
id: 'secondString',
|
||||
inputs: Object,
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'procedures_call',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
rt.blocks.createBlock(block1);
|
||||
rt.blocks.createBlock(block2);
|
||||
rt.blocks.createBlock(block2);
|
||||
th.target = rt;
|
||||
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
th.pushStack('secondString');
|
||||
t.strictEquals(th.peekStack(), 'secondString');
|
||||
th.goToNextBlock();
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
th.pushStack('secondString');
|
||||
th.pushStack('arbitraryString');
|
||||
t.strictEquals(th.peekStack(), 'arbitraryString');
|
||||
th.goToNextBlock();
|
||||
t.strictEquals(th.peekStack(), 'secondString');
|
||||
th.goToNextBlock();
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stopThisScript', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const rt = new RenderedTarget(s, r);
|
||||
const block1 = {fields: Object,
|
||||
id: 'arbitraryString',
|
||||
inputs: Object,
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'motion_movesteps',
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
const block2 = {fields: Object,
|
||||
id: 'secondString',
|
||||
inputs: Object,
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'procedures_call',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
rt.blocks.createBlock(block1);
|
||||
rt.blocks.createBlock(block2);
|
||||
th.target = rt;
|
||||
|
||||
th.stopThisScript();
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
th.pushStack('arbitraryString');
|
||||
t.strictEquals(th.peekStack(), 'arbitraryString');
|
||||
th.stopThisScript();
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
th.pushStack('arbitraryString');
|
||||
th.pushStack('secondString');
|
||||
th.stopThisScript();
|
||||
t.strictEquals(th.peekStack(), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isRecursiveCall', t => {
|
||||
const th = new Thread('arbitraryString');
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const rt = new RenderedTarget(s, r);
|
||||
const block1 = {fields: Object,
|
||||
id: 'arbitraryString',
|
||||
inputs: Object,
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'motion_movesteps',
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
const block2 = {fields: Object,
|
||||
id: 'secondString',
|
||||
inputs: Object,
|
||||
STEPS: Object,
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'procedures_call',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
rt.blocks.createBlock(block1);
|
||||
rt.blocks.createBlock(block2);
|
||||
th.target = rt;
|
||||
|
||||
const pushStack = id => {
|
||||
th.pushStack(id);
|
||||
th.peekStackFrame().op = {id};
|
||||
};
|
||||
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), false);
|
||||
pushStack('secondString');
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), false);
|
||||
pushStack('arbitraryString');
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), true);
|
||||
pushStack('arbitraryString');
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), true);
|
||||
th.popStack();
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), true);
|
||||
th.popStack();
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), false);
|
||||
th.popStack();
|
||||
t.strictEquals(th.isRecursiveCall('fakeCode'), false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
110
scratch-vm/test/unit/engine_variable.js
Normal file
110
scratch-vm/test/unit/engine_variable.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const test = require('tap').test;
|
||||
const Variable = require('../../src/engine/variable');
|
||||
const htmlparser = require('htmlparser2');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(typeof Variable.SCALAR_TYPE, typeof Variable.LIST_TYPE);
|
||||
t.type(typeof Variable.SCALAR_TYPE, typeof Variable.BROADCAST_MESSAGE_TYPE);
|
||||
|
||||
const varId = 'varId';
|
||||
const varName = 'varName';
|
||||
const varIsCloud = false;
|
||||
let v = new Variable(
|
||||
varId,
|
||||
varName,
|
||||
Variable.SCALAR_TYPE,
|
||||
varIsCloud
|
||||
);
|
||||
|
||||
t.type(Variable, 'function');
|
||||
t.type(v, 'object');
|
||||
t.ok(v instanceof Variable);
|
||||
|
||||
t.equal(v.id, varId);
|
||||
t.equal(v.name, varName);
|
||||
t.equal(v.type, Variable.SCALAR_TYPE);
|
||||
t.type(v.value, 'number');
|
||||
t.equal(v.isCloud, varIsCloud);
|
||||
|
||||
t.type(v.toXML, 'function');
|
||||
|
||||
v = new Variable(
|
||||
varId,
|
||||
varName,
|
||||
Variable.LIST_TYPE,
|
||||
varIsCloud
|
||||
);
|
||||
t.ok(Array.isArray(v.value));
|
||||
|
||||
v = new Variable(
|
||||
varId,
|
||||
varName,
|
||||
Variable.BROADCAST_MESSAGE_TYPE,
|
||||
varIsCloud
|
||||
);
|
||||
t.equal(v.value, 'varName');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('toXML', t => {
|
||||
const varId = 'varId';
|
||||
const varName = 'varName';
|
||||
const varIsCloud = false;
|
||||
const varIsLocal = false;
|
||||
const v = new Variable(
|
||||
varId,
|
||||
varName,
|
||||
Variable.SCALAR_TYPE,
|
||||
varIsCloud
|
||||
);
|
||||
|
||||
const parser = new htmlparser.Parser({
|
||||
onopentag: function (name, attribs){
|
||||
if (name === 'variable'){
|
||||
t.equal(attribs.type, Variable.SCALAR_TYPE);
|
||||
t.equal(attribs.id, varId);
|
||||
t.equal(attribs.iscloud, varIsCloud.toString());
|
||||
t.equal(attribs.islocal, varIsLocal.toString());
|
||||
}
|
||||
},
|
||||
ontext: function (text){
|
||||
t.equal(text, varName);
|
||||
}
|
||||
}, {decodeEntities: false});
|
||||
parser.write(v.toXML(false));
|
||||
parser.end();
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('escape variable name for XML', t => {
|
||||
const varId = 'varId';
|
||||
const varName = '<>&\'"';
|
||||
const varIsCloud = false;
|
||||
const varIsLocal = false;
|
||||
const v = new Variable(
|
||||
varId,
|
||||
varName,
|
||||
Variable.SCALAR_TYPE,
|
||||
varIsCloud
|
||||
);
|
||||
|
||||
const parser = new htmlparser.Parser({
|
||||
onopentag: function (name, attribs){
|
||||
if (name === 'variable'){
|
||||
t.equal(attribs.type, Variable.SCALAR_TYPE);
|
||||
t.equal(attribs.id, varId);
|
||||
t.equal(attribs.iscloud, varIsCloud.toString());
|
||||
t.equal(attribs.islocal, varIsLocal.toString());
|
||||
}
|
||||
},
|
||||
ontext: function (text){
|
||||
t.equal(text, '<>&'"');
|
||||
}
|
||||
}, {decodeEntities: false});
|
||||
parser.write(v.toXML(false));
|
||||
parser.end();
|
||||
|
||||
t.end();
|
||||
});
|
||||
327
scratch-vm/test/unit/extension_conversion.js
Normal file
327
scratch-vm/test/unit/extension_conversion.js
Normal file
@@ -0,0 +1,327 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const ScratchBlocksConstants = require('../../src/engine/scratch-blocks-constants');
|
||||
|
||||
/**
|
||||
* @type {ExtensionMetadata}
|
||||
*/
|
||||
const testExtensionInfo = {
|
||||
id: 'test',
|
||||
name: 'fake test extension',
|
||||
color1: '#111111',
|
||||
color2: '#222222',
|
||||
color3: '#333333',
|
||||
blocks: [
|
||||
{
|
||||
func: 'MAKE_A_VARIABLE',
|
||||
blockType: BlockType.BUTTON,
|
||||
text: 'this is a button'
|
||||
},
|
||||
{
|
||||
opcode: 'reporter',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'simple text',
|
||||
blockIconURI: 'invalid icon URI' // trigger the 'scratch_extension' path
|
||||
},
|
||||
{
|
||||
opcode: 'inlineImage',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'text and [IMAGE]',
|
||||
arguments: {
|
||||
IMAGE: {
|
||||
type: ArgumentType.IMAGE,
|
||||
dataURI: 'invalid image URI'
|
||||
}
|
||||
}
|
||||
},
|
||||
'---', // separator between groups of blocks in an extension
|
||||
{
|
||||
opcode: 'command',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'text with [ARG] [ARG_WITH_DEFAULT]',
|
||||
arguments: {
|
||||
ARG: {
|
||||
type: ArgumentType.STRING
|
||||
},
|
||||
ARG_WITH_DEFAULT: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: 'default text'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'ifElse',
|
||||
blockType: BlockType.CONDITIONAL,
|
||||
branchCount: 2,
|
||||
text: [
|
||||
'test if [THING] is spiffy and if so then',
|
||||
'or elsewise'
|
||||
],
|
||||
arguments: {
|
||||
THING: {
|
||||
type: ArgumentType.BOOLEAN
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'loop',
|
||||
blockType: BlockType.LOOP, // implied branchCount of 1 unless otherwise stated
|
||||
isTerminal: true,
|
||||
text: [
|
||||
'loopty [MANY] loops'
|
||||
],
|
||||
arguments: {
|
||||
MANY: {
|
||||
type: ArgumentType.NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const extensionInfoWithCustomFieldTypes = {
|
||||
id: 'test_custom_fieldType',
|
||||
name: 'fake test extension with customFieldTypes',
|
||||
color1: '#111111',
|
||||
color2: '#222222',
|
||||
color3: '#333333',
|
||||
blocks: [
|
||||
{ // Block that uses custom field types
|
||||
opcode: 'motorTurnFor',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: '[PORT] run [DIRECTION] for [VALUE] [UNIT]',
|
||||
arguments: {
|
||||
PORT: {
|
||||
defaultValue: 'A',
|
||||
type: 'single-port-selector'
|
||||
},
|
||||
DIRECTION: {
|
||||
defaultValue: 'clockwise',
|
||||
type: 'custom-direction'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
customFieldTypes: {
|
||||
'single-port-selector': {
|
||||
output: 'string',
|
||||
outputShape: 2,
|
||||
implementation: {
|
||||
fromJson: () => null
|
||||
}
|
||||
},
|
||||
'custom-direction': {
|
||||
output: 'string',
|
||||
outputShape: 3,
|
||||
implementation: {
|
||||
fromJson: () => null
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testCategoryInfo = function (t, block) {
|
||||
t.equal(block.json.category, 'fake test extension');
|
||||
t.equal(block.json.colour, '#111111');
|
||||
t.equal(block.json.colourSecondary, '#222222');
|
||||
t.equal(block.json.colourTertiary, '#333333');
|
||||
t.equal(block.json.inputsInline, true);
|
||||
};
|
||||
|
||||
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');
|
||||
testCategoryInfo(t, reporter);
|
||||
t.equal(reporter.json.checkboxInFlyout, true);
|
||||
t.equal(reporter.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND);
|
||||
t.equal(reporter.json.output, 'String');
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(reporter.json, 'previousStatement'));
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(reporter.json, 'nextStatement'));
|
||||
t.same(reporter.json.extensions, ['from_extension', 'scratch_extension']);
|
||||
t.equal(reporter.json.message0, '%1 %2simple text'); // "%1 %2" from the block icon
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(reporter.json, 'message1'));
|
||||
t.same(reporter.json.args0, [
|
||||
// %1 in message0: the block icon
|
||||
{
|
||||
type: 'field_image',
|
||||
src: 'invalid icon URI',
|
||||
width: 40,
|
||||
height: 40
|
||||
},
|
||||
// %2 in message0: separator between icon and text (only added when there's also an icon)
|
||||
{
|
||||
type: 'field_vertical_separator'
|
||||
}
|
||||
]);
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(reporter.json, 'args1'));
|
||||
t.equal(reporter.xml, '<block type="test_reporter"></block>');
|
||||
};
|
||||
|
||||
const testInlineImage = function (t, inlineImage) {
|
||||
t.equal(inlineImage.json.type, 'test_inlineImage');
|
||||
testCategoryInfo(t, inlineImage);
|
||||
t.equal(inlineImage.json.checkboxInFlyout, true);
|
||||
t.equal(inlineImage.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND);
|
||||
t.equal(inlineImage.json.output, 'String');
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(inlineImage.json, 'previousStatement'));
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(inlineImage.json, 'nextStatement'));
|
||||
t.same(inlineImage.json.extensions, ['from_extension']);
|
||||
t.equal(inlineImage.json.message0, 'text and %1'); // block text followed by inline image
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(inlineImage.json, 'message1'));
|
||||
t.same(inlineImage.json.args0, [
|
||||
// %1 in message0: the block icon
|
||||
{
|
||||
type: 'field_image',
|
||||
src: 'invalid image URI',
|
||||
width: 24,
|
||||
height: 24,
|
||||
flip_rtl: false // False by default
|
||||
}
|
||||
]);
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(inlineImage.json, 'args1'));
|
||||
t.equal(inlineImage.xml, '<block type="test_inlineImage"></block>');
|
||||
};
|
||||
|
||||
const testSeparator = function (t, separator) {
|
||||
t.same(separator.json, null); // should be null or undefined
|
||||
t.equal(separator.xml, '<sep gap="36"/>');
|
||||
};
|
||||
|
||||
const testCommand = function (t, command) {
|
||||
t.equal(command.json.type, 'test_command');
|
||||
testCategoryInfo(t, command);
|
||||
t.equal(command.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
|
||||
t.assert(Object.prototype.hasOwnProperty.call(command.json, 'previousStatement'));
|
||||
t.assert(Object.prototype.hasOwnProperty.call(command.json, 'nextStatement'));
|
||||
t.same(command.json.extensions, ['from_extension']);
|
||||
t.equal(command.json.message0, 'text with %1 %2');
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(command.json, 'message1'));
|
||||
t.strictSame(command.json.args0[0], {
|
||||
type: 'input_value',
|
||||
name: 'ARG'
|
||||
});
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(command.json, 'args1'));
|
||||
t.equal(command.xml,
|
||||
'<block type="test_command"><value name="ARG"><shadow type="text"></shadow></value>' +
|
||||
'<value name="ARG_WITH_DEFAULT"><shadow type="text"><field name="TEXT">' +
|
||||
'default text</field></shadow></value></block>');
|
||||
};
|
||||
|
||||
const testConditional = function (t, conditional) {
|
||||
t.equal(conditional.json.type, 'test_ifElse');
|
||||
testCategoryInfo(t, conditional);
|
||||
t.equal(conditional.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
|
||||
t.ok(Object.prototype.hasOwnProperty.call(conditional.json, 'previousStatement'));
|
||||
t.ok(Object.prototype.hasOwnProperty.call(conditional.json, 'nextStatement'));
|
||||
t.same(conditional.json.extensions, ['from_extension']);
|
||||
t.equal(conditional.json.message0, 'test if %1 is spiffy and if so then');
|
||||
t.equal(conditional.json.message1, '%1'); // placeholder for substack #1
|
||||
t.equal(conditional.json.message2, 'or elsewise');
|
||||
t.equal(conditional.json.message3, '%1'); // placeholder for substack #2
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(conditional.json, 'message4'));
|
||||
t.strictSame(conditional.json.args0[0], {
|
||||
type: 'input_value',
|
||||
name: 'THING',
|
||||
check: 'Boolean'
|
||||
});
|
||||
t.strictSame(conditional.json.args1[0], {
|
||||
type: 'input_statement',
|
||||
name: 'SUBSTACK'
|
||||
});
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(conditional.json, conditional.json.args2));
|
||||
t.strictSame(conditional.json.args3[0], {
|
||||
type: 'input_statement',
|
||||
name: 'SUBSTACK2'
|
||||
});
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(conditional.json, 'args4'));
|
||||
t.equal(conditional.xml, '<block type="test_ifElse"><value name="THING"></value></block>');
|
||||
};
|
||||
|
||||
const testLoop = function (t, loop) {
|
||||
t.equal(loop.json.type, 'test_loop');
|
||||
testCategoryInfo(t, loop);
|
||||
t.equal(loop.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
|
||||
t.ok(Object.prototype.hasOwnProperty.call(loop.json, 'previousStatement'));
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(loop.json, 'nextStatement')); // isTerminal is set on this block
|
||||
t.same(loop.json.extensions, ['from_extension']);
|
||||
t.equal(loop.json.message0, 'loopty %1 loops');
|
||||
t.equal(loop.json.message1, '%1'); // placeholder for substack
|
||||
t.equal(loop.json.message2, '%1'); // placeholder for loop arrow
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(loop.json, 'message3'));
|
||||
t.strictSame(loop.json.args0[0], {
|
||||
type: 'input_value',
|
||||
name: 'MANY'
|
||||
});
|
||||
t.strictSame(loop.json.args1[0], {
|
||||
type: 'input_statement',
|
||||
name: 'SUBSTACK'
|
||||
});
|
||||
t.equal(loop.json.lastDummyAlign2, 'RIGHT'); // move loop arrow to right side
|
||||
t.equal(loop.json.args2[0].type, 'field_image');
|
||||
t.equal(loop.json.args2[0].flip_rtl, true);
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(loop.json, 'args3'));
|
||||
t.equal(loop.xml,
|
||||
'<block type="test_loop"><value name="MANY"><shadow type="math_number"></shadow></value></block>');
|
||||
};
|
||||
|
||||
test('registerExtensionPrimitives', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
|
||||
const blocksInfo = categoryInfo.blocks;
|
||||
t.equal(blocksInfo.length, testExtensionInfo.blocks.length);
|
||||
|
||||
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, inlineImage, separator, command, conditional, loop] = blocksInfo;
|
||||
|
||||
testButton(t, button);
|
||||
testReporter(t, reporter);
|
||||
testInlineImage(t, inlineImage);
|
||||
testSeparator(t, separator);
|
||||
testCommand(t, command);
|
||||
testConditional(t, conditional);
|
||||
testLoop(t, loop);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
runtime._registerExtensionPrimitives(testExtensionInfo);
|
||||
});
|
||||
|
||||
test('custom field types should be added to block and EXTENSION_FIELD_ADDED callback triggered', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
|
||||
const blockInfo = categoryInfo.blocks[0];
|
||||
|
||||
// We expect that for each argument there's a corresponding <field>-tag in the block XML
|
||||
Object.values(blockInfo.info.arguments).forEach(argument => {
|
||||
const regex = new RegExp(`<field name="field_${categoryInfo.id}_${argument.type}">`);
|
||||
t.true(regex.test(blockInfo.xml));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
let fieldAddedCallbacks = 0;
|
||||
runtime.on(Runtime.EXTENSION_FIELD_ADDED, () => {
|
||||
fieldAddedCallbacks++;
|
||||
});
|
||||
|
||||
runtime._registerExtensionPrimitives(extensionInfoWithCustomFieldTypes);
|
||||
|
||||
// Extension includes two custom field types
|
||||
t.equal(fieldAddedCallbacks, 2);
|
||||
t.end();
|
||||
});
|
||||
12
scratch-vm/test/unit/extension_microbit.js
Normal file
12
scratch-vm/test/unit/extension_microbit.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const test = require('tap').test;
|
||||
// const MicroBit = require('../../src/extensions/scratch3_microbit/index.js');
|
||||
|
||||
test('displayText', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('displayMatrix', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
// etc...
|
||||
49
scratch-vm/test/unit/extension_music.js
Normal file
49
scratch-vm/test/unit/extension_music.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const test = require('tap').test;
|
||||
const Music = require('../../src/extensions/scratch3_music/index.js');
|
||||
|
||||
const fakeRuntime = {
|
||||
getTargetForStage: () => ({tempo: 60}),
|
||||
on: () => {} // Stub out listener methods used in constructor.
|
||||
};
|
||||
|
||||
const blocks = new Music(fakeRuntime);
|
||||
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
target: {
|
||||
audioPlayer: null
|
||||
},
|
||||
yield: () => null
|
||||
};
|
||||
|
||||
test('playDrum uses 1-indexing and wrap clamps', t => {
|
||||
// Stub playDrumNum
|
||||
let playedDrum;
|
||||
blocks._playDrumNum = (_util, drum) => (playedDrum = drum);
|
||||
|
||||
let args = {DRUM: 1};
|
||||
blocks.playDrumForBeats(args, util);
|
||||
t.strictEqual(playedDrum, 0);
|
||||
|
||||
args = {DRUM: blocks.DRUM_INFO.length + 1};
|
||||
blocks.playDrumForBeats(args, util);
|
||||
t.strictEqual(playedDrum, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setInstrument uses 1-indexing and wrap clamps', t => {
|
||||
// Stub getMusicState
|
||||
const state = {currentInstrument: 0};
|
||||
blocks._getMusicState = () => state;
|
||||
|
||||
let args = {INSTRUMENT: 1};
|
||||
blocks.setInstrument(args, util);
|
||||
t.strictEqual(state.currentInstrument, 0);
|
||||
|
||||
args = {INSTRUMENT: blocks.INSTRUMENT_INFO.length + 1};
|
||||
blocks.setInstrument(args, util);
|
||||
t.strictEqual(state.currentInstrument, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
44
scratch-vm/test/unit/extension_text_to_speech.js
Normal file
44
scratch-vm/test/unit/extension_text_to_speech.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const test = require('tap').test;
|
||||
const TextToSpeech = require('../../src/extensions/scratch3_text2speech/index.js');
|
||||
|
||||
const fakeStage = {
|
||||
textToSpeechLanguage: null
|
||||
};
|
||||
|
||||
const fakeRuntime = {
|
||||
getTargetForStage: () => fakeStage,
|
||||
on: () => {} // Stub out listener methods used in constructor.
|
||||
};
|
||||
|
||||
const ext = new TextToSpeech(fakeRuntime);
|
||||
|
||||
test('if no language is saved in the project, use default', t => {
|
||||
t.strictEqual(ext.getCurrentLanguage(), 'en');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('if an unsupported language is dropped onto the set language block, use default', t => {
|
||||
ext.setLanguage({LANGUAGE: 'nope'});
|
||||
t.strictEqual(ext.getCurrentLanguage(), 'en');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('if a supported language name is dropped onto the set language block, use it', t => {
|
||||
ext.setLanguage({LANGUAGE: 'español'});
|
||||
t.strictEqual(ext.getCurrentLanguage(), 'es');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('get the extension locale for a supported locale that differs', t => {
|
||||
ext.setLanguage({LANGUAGE: 'ja-hira'});
|
||||
t.strictEqual(ext.getCurrentLanguage(), 'ja');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('use localized spoken language name in place of localized written language name', t => {
|
||||
ext.getEditorLanguage = () => 'es';
|
||||
const languageMenu = ext.getLanguageMenu();
|
||||
const localizedNameForChineseInSpanish = languageMenu.find(el => el.value === 'zh-cn').text;
|
||||
t.strictEqual(localizedNameForChineseInSpanish, 'Chino (Mandarín)'); // i.e. should not be 'Chino (simplificado)'
|
||||
t.end();
|
||||
});
|
||||
411
scratch-vm/test/unit/extension_video_sensing.js
Normal file
411
scratch-vm/test/unit/extension_video_sensing.js
Normal file
@@ -0,0 +1,411 @@
|
||||
const {createReadStream} = require('fs');
|
||||
const {join} = require('path');
|
||||
|
||||
const {PNG} = require('pngjs');
|
||||
const {test} = require('tap');
|
||||
|
||||
const {wrapClamp} = require('../../src/util/math-util');
|
||||
|
||||
const VideoSensing = require('../../src/extensions/scratch3_video_sensing/index.js');
|
||||
const VideoMotion = require('../../src/extensions/scratch3_video_sensing/library.js');
|
||||
|
||||
/**
|
||||
* Prefix to the mock frame images used to test the video sensing extension.
|
||||
* @type {string}
|
||||
*/
|
||||
const pngPrefix = 'extension_video_sensing_';
|
||||
|
||||
/**
|
||||
* Map of frame keys to the image filenames appended to the pngPrefix.
|
||||
* @type {object}
|
||||
*/
|
||||
const framesMap = {
|
||||
center: 'center',
|
||||
left: 'left-5',
|
||||
left2: 'left-10',
|
||||
down: 'down-10'
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously read a png file and copy its pixel data into a typed array
|
||||
* VideoMotion will accept.
|
||||
* @param {string} name - partial filename to read
|
||||
* @returns {Promise.<Uint32Array>} pixel data of the image
|
||||
*/
|
||||
const readPNG = name => (
|
||||
new Promise((resolve, reject) => {
|
||||
const png = new PNG();
|
||||
createReadStream(join(__dirname, `${pngPrefix}${name}.png`))
|
||||
.pipe(png)
|
||||
.on('parsed', () => {
|
||||
// Copy the RGBA pixel values into a separate typed array and
|
||||
// cast the array to Uint32, the array format VideoMotion takes.
|
||||
resolve(new Uint32Array(new Uint8ClampedArray(png.data).buffer));
|
||||
})
|
||||
.on('error', reject);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Read all the frames for testing asynchrnously and produce an object with
|
||||
* keys following the keys in framesMap.
|
||||
* @returns {object} mapping of keys in framesMap to image data read from disk
|
||||
*/
|
||||
const readFrames = (() => {
|
||||
// Use this immediately invoking function expression (IIFE) to delay reading
|
||||
// once to the first test that calls readFrames.
|
||||
let _promise = null;
|
||||
|
||||
return () => {
|
||||
if (_promise === null) {
|
||||
_promise = Promise.all(Object.keys(framesMap).map(key => readPNG(framesMap[key])))
|
||||
.then(pngs => (
|
||||
Object.keys(framesMap).reduce((frames, key, i) => {
|
||||
frames[key] = pngs[i];
|
||||
return frames;
|
||||
}, {})
|
||||
));
|
||||
}
|
||||
return _promise;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Match if actual is within optMargin to expect. If actual is under -180,
|
||||
* match if actual + 360 is near expect. If actual is over 180, match if actual
|
||||
* - 360 is near expect.
|
||||
* @param {number} actual - actual angle in degrees
|
||||
* @param {number} expect - expected angle in degrees
|
||||
* @param {number} optMargin - allowed margin between actual and expect in degrees
|
||||
* @returns {boolean} true if actual is close to expect
|
||||
*/
|
||||
const isNearAngle = (actual, expect, optMargin = 10) => (
|
||||
(wrapClamp(actual - expect, 0, 359) < optMargin) ||
|
||||
(wrapClamp(actual - expect, 0, 359) > 360 - optMargin)
|
||||
);
|
||||
|
||||
// A fake scratch-render drawable that will be used by VideoMotion to restrain
|
||||
// the area considered for motion detection in VideoMotion.getLocalMotion
|
||||
const fakeDrawable = {
|
||||
updateCPURenderAttributes () {}, // no-op, since isTouching always returns true
|
||||
|
||||
getFastBounds () {
|
||||
return {
|
||||
left: -120,
|
||||
top: 60,
|
||||
right: 0,
|
||||
bottom: -60
|
||||
};
|
||||
},
|
||||
|
||||
isTouching () {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// A fake MotionState used to test the stored values in
|
||||
// VideoMotion.getLocalMotion, VideoSensing.videoOn and
|
||||
// VideoSensing.whenMotionGreaterThan.
|
||||
const fakeMotionState = {
|
||||
motionFrameNumber: -1,
|
||||
motionAmount: -1,
|
||||
motionDirection: -Infinity
|
||||
};
|
||||
|
||||
// A fake target referring to the fake drawable and MotionState.
|
||||
const fakeTarget = {
|
||||
drawableID: 0,
|
||||
|
||||
getCustomState () {
|
||||
return fakeMotionState;
|
||||
},
|
||||
setCustomState () {}
|
||||
};
|
||||
|
||||
const fakeRuntime = {
|
||||
targets: [fakeTarget],
|
||||
|
||||
// Without defined devices, VideoSensing will not try to start sampling from
|
||||
// a video source.
|
||||
ioDevices: null,
|
||||
|
||||
renderer: {
|
||||
_allDrawables: [
|
||||
fakeDrawable
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const fakeBlockUtility = {
|
||||
target: fakeTarget
|
||||
};
|
||||
|
||||
test('detect motionAmount between frames', t => {
|
||||
t.plan(6);
|
||||
|
||||
return readFrames()
|
||||
.then(frames => {
|
||||
const detect = new VideoMotion();
|
||||
|
||||
// Each of these pairs should have enough motion for the detector.
|
||||
const framePairs = [
|
||||
[frames.center, frames.left],
|
||||
[frames.center, frames.left2],
|
||||
[frames.left, frames.left2],
|
||||
[frames.left, frames.center],
|
||||
[frames.center, frames.down],
|
||||
[frames.down, frames.center]
|
||||
];
|
||||
|
||||
// Add both frames of a pair and test for motion.
|
||||
let index = 0;
|
||||
for (const [frame1, frame2] of framePairs) {
|
||||
detect.addFrame(frame1);
|
||||
detect.addFrame(frame2);
|
||||
|
||||
detect.analyzeFrame();
|
||||
t.ok(
|
||||
detect.motionAmount > 10,
|
||||
`frame pair ${index + 1} has motion ${detect.motionAmount} over threshold (10)`
|
||||
);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('detect local motionAmount between frames', t => {
|
||||
t.plan(6);
|
||||
|
||||
return readFrames()
|
||||
.then(frames => {
|
||||
const detect = new VideoMotion();
|
||||
|
||||
// Each of these pairs should have enough motion for the detector.
|
||||
const framePairs = [
|
||||
[frames.center, frames.left],
|
||||
[frames.center, frames.left2],
|
||||
[frames.left, frames.left2],
|
||||
[frames.left, frames.center],
|
||||
[frames.center, frames.down],
|
||||
[frames.down, frames.center]
|
||||
];
|
||||
|
||||
// Add both frames of a pair and test for local motion.
|
||||
let index = 0;
|
||||
for (const [frame1, frame2] of framePairs) {
|
||||
detect.addFrame(frame1);
|
||||
detect.addFrame(frame2);
|
||||
|
||||
detect.analyzeFrame();
|
||||
detect.getLocalMotion(fakeDrawable, fakeMotionState);
|
||||
t.ok(
|
||||
fakeMotionState.motionAmount > 10,
|
||||
`frame pair ${index + 1} has motion ${fakeMotionState.motionAmount} over threshold (10)`
|
||||
);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('detect motionDirection between frames', t => {
|
||||
t.plan(6);
|
||||
|
||||
return readFrames()
|
||||
.then(frames => {
|
||||
const detect = new VideoMotion();
|
||||
|
||||
// Each of these pairs is moving in the given direction. Does the detector
|
||||
// guess a value to that?
|
||||
const directionMargin = 10;
|
||||
const framePairs = [
|
||||
{
|
||||
frames: [frames.center, frames.left],
|
||||
direction: -90
|
||||
},
|
||||
{
|
||||
frames: [frames.center, frames.left2],
|
||||
direction: -90
|
||||
},
|
||||
{
|
||||
frames: [frames.left, frames.left2],
|
||||
direction: -90
|
||||
},
|
||||
{
|
||||
frames: [frames.left, frames.center],
|
||||
direction: 90
|
||||
},
|
||||
{
|
||||
frames: [frames.center, frames.down],
|
||||
direction: 180
|
||||
},
|
||||
{
|
||||
frames: [frames.down, frames.center],
|
||||
direction: 0
|
||||
}
|
||||
];
|
||||
|
||||
// Add both frames of a pair and check if the motionDirection is near the
|
||||
// expected angle.
|
||||
let index = 0;
|
||||
for (const {frames: [frame1, frame2], direction} of framePairs) {
|
||||
detect.addFrame(frame1);
|
||||
detect.addFrame(frame2);
|
||||
|
||||
detect.analyzeFrame();
|
||||
t.ok(
|
||||
isNearAngle(detect.motionDirection, direction, directionMargin),
|
||||
`frame pair ${index + 1} is ${detect.motionDirection.toFixed(0)} ` +
|
||||
`degrees and close to ${direction} degrees`
|
||||
);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('detect local motionDirection between frames', t => {
|
||||
t.plan(6);
|
||||
|
||||
return readFrames()
|
||||
.then(frames => {
|
||||
const detect = new VideoMotion();
|
||||
|
||||
// Each of these pairs is moving in the given direction. Does the detector
|
||||
// guess a value to that?
|
||||
const directionMargin = 10;
|
||||
const framePairs = [
|
||||
{
|
||||
frames: [frames.center, frames.left],
|
||||
direction: -90
|
||||
},
|
||||
{
|
||||
frames: [frames.center, frames.left2],
|
||||
direction: -90
|
||||
},
|
||||
{
|
||||
frames: [frames.left, frames.left2],
|
||||
direction: -90
|
||||
},
|
||||
{
|
||||
frames: [frames.left, frames.center],
|
||||
direction: 90
|
||||
},
|
||||
{
|
||||
frames: [frames.center, frames.down],
|
||||
direction: 180
|
||||
},
|
||||
{
|
||||
frames: [frames.down, frames.center],
|
||||
direction: 0
|
||||
}
|
||||
];
|
||||
|
||||
// Add both frames of a pair and check if the local motionDirection is near
|
||||
// the expected angle.
|
||||
let index = 0;
|
||||
for (const {frames: [frame1, frame2], direction} of framePairs) {
|
||||
detect.addFrame(frame1);
|
||||
detect.addFrame(frame2);
|
||||
|
||||
detect.analyzeFrame();
|
||||
detect.getLocalMotion(fakeDrawable, fakeMotionState);
|
||||
const motionDirection = fakeMotionState.motionDirection;
|
||||
t.ok(
|
||||
isNearAngle(motionDirection, direction, directionMargin),
|
||||
`frame pair ${index + 1} is ${motionDirection.toFixed(0)} degrees and close to ${direction} degrees`
|
||||
);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('videoOn returns value dependent on arguments', t => {
|
||||
t.plan(4);
|
||||
|
||||
return readFrames()
|
||||
.then(frames => {
|
||||
const sensing = new VideoSensing(fakeRuntime);
|
||||
|
||||
// With these two frame test if we get expected values depending on the
|
||||
// arguments to videoOn.
|
||||
sensing.detect.addFrame(frames.center);
|
||||
sensing.detect.addFrame(frames.left);
|
||||
|
||||
const motionAmount = sensing.videoOn({
|
||||
ATTRIBUTE: VideoSensing.SensingAttribute.MOTION,
|
||||
SUBJECT: VideoSensing.SensingSubject.STAGE
|
||||
}, fakeBlockUtility);
|
||||
t.ok(
|
||||
motionAmount > 10,
|
||||
`stage motionAmount ${motionAmount} is over the threshold (10)`
|
||||
);
|
||||
|
||||
const localMotionAmount = sensing.videoOn({
|
||||
ATTRIBUTE: VideoSensing.SensingAttribute.MOTION,
|
||||
SUBJECT: VideoSensing.SensingSubject.SPRITE
|
||||
}, fakeBlockUtility);
|
||||
t.ok(
|
||||
localMotionAmount > 10,
|
||||
`sprite motionAmount ${localMotionAmount} is over the threshold (10)`
|
||||
);
|
||||
|
||||
const motionDirection = sensing.videoOn({
|
||||
ATTRIBUTE: VideoSensing.SensingAttribute.DIRECTION,
|
||||
SUBJECT: VideoSensing.SensingSubject.STAGE
|
||||
}, fakeBlockUtility);
|
||||
t.ok(
|
||||
isNearAngle(motionDirection, -90),
|
||||
`stage motionDirection ${motionDirection.toFixed(0)} degrees is close to ${90} degrees`
|
||||
);
|
||||
|
||||
const localMotionDirection = sensing.videoOn({
|
||||
ATTRIBUTE: VideoSensing.SensingAttribute.DIRECTION,
|
||||
SUBJECT: VideoSensing.SensingSubject.SPRITE
|
||||
}, fakeBlockUtility);
|
||||
t.ok(
|
||||
isNearAngle(localMotionDirection, -90),
|
||||
`sprite motionDirection ${localMotionDirection.toFixed(0)} degrees is close to ${90} degrees`
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('whenMotionGreaterThan returns true if local motion meets target', t => {
|
||||
t.plan(2);
|
||||
|
||||
return readFrames()
|
||||
.then(frames => {
|
||||
const sensing = new VideoSensing(fakeRuntime);
|
||||
|
||||
// With these two frame test if we get expected values depending on the
|
||||
// arguments to whenMotionGreaterThan.
|
||||
sensing.detect.addFrame(frames.center);
|
||||
sensing.detect.addFrame(frames.left);
|
||||
|
||||
const over20 = sensing.whenMotionGreaterThan({
|
||||
REFERENCE: 20
|
||||
}, fakeBlockUtility);
|
||||
t.ok(
|
||||
over20,
|
||||
`enough motion in drawable bounds to reach reference of 20`
|
||||
);
|
||||
|
||||
const over80 = sensing.whenMotionGreaterThan({
|
||||
REFERENCE: 80
|
||||
}, fakeBlockUtility);
|
||||
t.notOk(
|
||||
over80,
|
||||
`not enough motion in drawable bounds to reach reference of 80`
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
BIN
scratch-vm/test/unit/extension_video_sensing_center.png
Normal file
BIN
scratch-vm/test/unit/extension_video_sensing_center.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
scratch-vm/test/unit/extension_video_sensing_down-10.png
Normal file
BIN
scratch-vm/test/unit/extension_video_sensing_down-10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
scratch-vm/test/unit/extension_video_sensing_left-10.png
Normal file
BIN
scratch-vm/test/unit/extension_video_sensing_left-10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
BIN
scratch-vm/test/unit/extension_video_sensing_left-5.png
Normal file
BIN
scratch-vm/test/unit/extension_video_sensing_left-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
37
scratch-vm/test/unit/io_clock.js
Normal file
37
scratch-vm/test/unit/io_clock.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const test = require('tap').test;
|
||||
const Clock = require('../../src/io/clock');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Clock(rt);
|
||||
|
||||
t.type(Clock, 'function');
|
||||
t.type(c, 'object');
|
||||
t.type(c.projectTimer, 'function');
|
||||
t.type(c.pause, 'function');
|
||||
t.type(c.resume, 'function');
|
||||
t.type(c.resetProjectTimer, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('cycle', t => {
|
||||
const rt = new Runtime();
|
||||
const c = new Clock(rt);
|
||||
|
||||
t.ok(c.projectTimer() <= 0.1);
|
||||
setTimeout(() => {
|
||||
c.resetProjectTimer();
|
||||
setTimeout(() => {
|
||||
// The timer shouldn't advance until all threads have been stepped
|
||||
t.ok(c.projectTimer() === 0);
|
||||
c.pause();
|
||||
t.ok(c.projectTimer() === 0);
|
||||
c.resume();
|
||||
t.ok(c.projectTimer() === 0);
|
||||
t.end();
|
||||
}, 100);
|
||||
}, 100);
|
||||
rt._step();
|
||||
t.ok(c.projectTimer() > 0);
|
||||
});
|
||||
168
scratch-vm/test/unit/io_cloud.js
Normal file
168
scratch-vm/test/unit/io_cloud.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const test = require('tap').test;
|
||||
const Cloud = require('../../src/io/cloud');
|
||||
const Target = require('../../src/engine/target');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', t => {
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
|
||||
t.type(cloud, 'object');
|
||||
t.type(cloud.postData, 'function');
|
||||
t.type(cloud.requestCreateVariable, 'function');
|
||||
t.type(cloud.requestUpdateVariable, 'function');
|
||||
t.type(cloud.requestRenameVariable, 'function');
|
||||
t.type(cloud.requestDeleteVariable, 'function');
|
||||
t.type(cloud.updateCloudVariable, 'function');
|
||||
t.type(cloud.setProvider, 'function');
|
||||
t.type(cloud.setStage, 'function');
|
||||
t.type(cloud.clear, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stage and provider are null initially', t => {
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
|
||||
t.strictEquals(cloud.provider, null);
|
||||
t.strictEquals(cloud.stage, null);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setProvider sets the provider', t => {
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
|
||||
const provider = {
|
||||
foo: 'a fake provider'
|
||||
};
|
||||
|
||||
cloud.setProvider(provider);
|
||||
t.strictEquals(cloud.provider, provider);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('postData update message updates the variable', t => {
|
||||
const runtime = new Runtime();
|
||||
const stage = new Target(runtime);
|
||||
const fooVar = new Variable(
|
||||
'a fake var id',
|
||||
'foo',
|
||||
Variable.SCALAR_TYPE,
|
||||
true /* isCloud */
|
||||
);
|
||||
stage.variables[fooVar.id] = fooVar;
|
||||
|
||||
t.strictEquals(fooVar.value, 0);
|
||||
|
||||
const cloud = new Cloud(runtime);
|
||||
cloud.setStage(stage);
|
||||
cloud.postData({varUpdate: {
|
||||
name: 'foo',
|
||||
value: 3
|
||||
}});
|
||||
t.strictEquals(fooVar.value, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('requestUpdateVariable calls provider\'s updateVariable function', t => {
|
||||
let updateVariableCalled = false;
|
||||
let mockVarName = '';
|
||||
let mockVarValue = '';
|
||||
const mockUpdateVariable = (name, value) => {
|
||||
updateVariableCalled = true;
|
||||
mockVarName = name;
|
||||
mockVarValue = value;
|
||||
return;
|
||||
};
|
||||
|
||||
const provider = {
|
||||
updateVariable: mockUpdateVariable
|
||||
};
|
||||
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
cloud.setProvider(provider);
|
||||
cloud.requestUpdateVariable('foo', 3);
|
||||
t.equals(updateVariableCalled, true);
|
||||
t.strictEquals(mockVarName, 'foo');
|
||||
t.strictEquals(mockVarValue, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('requestCreateVariable calls provider\'s createVariable function', t => {
|
||||
let createVariableCalled = false;
|
||||
const mockVariable = new Variable('a var id', 'my var', Variable.SCALAR_TYPE, false);
|
||||
let mockVarName;
|
||||
let mockVarValue;
|
||||
const mockCreateVariable = (name, value) => {
|
||||
createVariableCalled = true;
|
||||
mockVarName = name;
|
||||
mockVarValue = value;
|
||||
return;
|
||||
};
|
||||
|
||||
const provider = {
|
||||
createVariable: mockCreateVariable
|
||||
};
|
||||
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
cloud.setProvider(provider);
|
||||
cloud.requestCreateVariable(mockVariable);
|
||||
t.equals(createVariableCalled, true);
|
||||
t.strictEquals(mockVarName, 'my var');
|
||||
t.strictEquals(mockVarValue, 0);
|
||||
// Calling requestCreateVariable does not set isCloud flag on variable
|
||||
t.strictEquals(mockVariable.isCloud, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('requestRenameVariable calls provider\'s renameVariable function', t => {
|
||||
let renameVariableCalled = false;
|
||||
let mockVarOldName;
|
||||
let mockVarNewName;
|
||||
const mockRenameVariable = (oldName, newName) => {
|
||||
renameVariableCalled = true;
|
||||
mockVarOldName = oldName;
|
||||
mockVarNewName = newName;
|
||||
return;
|
||||
};
|
||||
|
||||
const provider = {
|
||||
renameVariable: mockRenameVariable
|
||||
};
|
||||
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
cloud.setProvider(provider);
|
||||
cloud.requestRenameVariable('my var', 'new var name');
|
||||
t.equals(renameVariableCalled, true);
|
||||
t.strictEquals(mockVarOldName, 'my var');
|
||||
t.strictEquals(mockVarNewName, 'new var name');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('requestDeleteVariable calls provider\'s deleteVariable function', t => {
|
||||
let deleteVariableCalled = false;
|
||||
let mockVarName;
|
||||
const mockDeleteVariable = name => {
|
||||
deleteVariableCalled = true;
|
||||
mockVarName = name;
|
||||
return;
|
||||
};
|
||||
|
||||
const provider = {
|
||||
deleteVariable: mockDeleteVariable
|
||||
};
|
||||
|
||||
const runtime = new Runtime();
|
||||
const cloud = new Cloud(runtime);
|
||||
cloud.setProvider(provider);
|
||||
cloud.requestDeleteVariable('my var');
|
||||
t.equals(deleteVariableCalled, true);
|
||||
t.strictEquals(mockVarName, 'my var');
|
||||
t.end();
|
||||
});
|
||||
105
scratch-vm/test/unit/io_keyboard.js
Normal file
105
scratch-vm/test/unit/io_keyboard.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const test = require('tap').test;
|
||||
const Keyboard = require('../../src/io/keyboard');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
t.type(k, 'object');
|
||||
t.type(k.postData, 'function');
|
||||
t.type(k.getKeyIsDown, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('space key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: ' ',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, ['space']);
|
||||
t.strictEquals(k.getKeyIsDown('space'), true);
|
||||
t.strictEquals(k.getKeyIsDown('any'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('letter key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: 'a',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, ['A']);
|
||||
t.strictEquals(k.getKeyIsDown(65), true);
|
||||
t.strictEquals(k.getKeyIsDown('a'), true);
|
||||
t.strictEquals(k.getKeyIsDown('A'), true);
|
||||
t.strictEquals(k.getKeyIsDown('any'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('number key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: '1',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, ['1']);
|
||||
t.strictEquals(k.getKeyIsDown(49), true);
|
||||
t.strictEquals(k.getKeyIsDown('1'), true);
|
||||
t.strictEquals(k.getKeyIsDown('any'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('non-english key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: '日',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, ['日']);
|
||||
t.strictEquals(k.getKeyIsDown('日'), true);
|
||||
t.strictEquals(k.getKeyIsDown('any'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
/* TW: This test is disabled because we intentionally add support for modifier keys.
|
||||
test('ignore modifier key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: 'Shift',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, []);
|
||||
t.strictEquals(k.getKeyIsDown('any'), false);
|
||||
t.end();
|
||||
});
|
||||
*/
|
||||
|
||||
test('keyup', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: 'ArrowLeft',
|
||||
isDown: true
|
||||
});
|
||||
k.postData({
|
||||
key: 'ArrowLeft',
|
||||
isDown: false
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, []);
|
||||
t.strictEquals(k.getKeyIsDown('left arrow'), false);
|
||||
t.strictEquals(k.getKeyIsDown('any'), false);
|
||||
t.end();
|
||||
});
|
||||
112
scratch-vm/test/unit/io_keyboard_tw.js
Normal file
112
scratch-vm/test/unit/io_keyboard_tw.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const test = require('tap').test;
|
||||
const Keyboard = require('../../src/io/keyboard');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('extended spec', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
t.type(k.getLastKeyPressed, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('extended key support', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: 'Backspace',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, ['backspace']);
|
||||
t.strictEqual(k.getKeyIsDown('backspace'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('last key pressed', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
t.strictEqual(k.getLastKeyPressed(), '');
|
||||
k.postData({
|
||||
key: 'a',
|
||||
isDown: true
|
||||
});
|
||||
t.strictEqual(k.getLastKeyPressed(), 'a');
|
||||
k.postData({
|
||||
key: 'b',
|
||||
isDown: true
|
||||
});
|
||||
t.strictEqual(k.getLastKeyPressed(), 'b');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('holding shift and key, releasing shift, then releasing key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
// Press Shift+2 to produce @
|
||||
k.postData({
|
||||
key: '@',
|
||||
isDown: true,
|
||||
keyCode: 50
|
||||
});
|
||||
t.equal(k.getKeyIsDown('2'), false);
|
||||
t.equal(k.getKeyIsDown('@'), true);
|
||||
t.equal(k.getKeyIsDown('any'), true);
|
||||
// Release shift, then release 2
|
||||
k.postData({
|
||||
key: 'Shift',
|
||||
isDown: false,
|
||||
keyCode: 16
|
||||
});
|
||||
k.postData({
|
||||
key: '2',
|
||||
isDown: false,
|
||||
keyCode: 50
|
||||
});
|
||||
t.equal(k.getKeyIsDown('@'), false);
|
||||
t.equal(k.getKeyIsDown('2'), false);
|
||||
t.equal(k.getKeyIsDown('any'), false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('holding shift and key, releasing shift, waiting, then releasing key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
key: '@',
|
||||
isDown: true,
|
||||
keyCode: 50
|
||||
});
|
||||
t.equal(k.getKeyIsDown('2'), false);
|
||||
t.equal(k.getKeyIsDown('@'), true);
|
||||
t.equal(k.getKeyIsDown('any'), true);
|
||||
k.postData({
|
||||
key: 'Shift',
|
||||
isDown: false,
|
||||
keyCode: 16
|
||||
});
|
||||
// But 2 is still being held, so it will send a press event
|
||||
k.postData({
|
||||
key: '2',
|
||||
isDown: true,
|
||||
keyCode: 50
|
||||
});
|
||||
t.equal(k.getKeyIsDown('@'), false);
|
||||
t.equal(k.getKeyIsDown('2'), true);
|
||||
t.equal(k.getKeyIsDown('any'), true);
|
||||
// And now we release 2
|
||||
k.postData({
|
||||
key: '2',
|
||||
isDown: false,
|
||||
keyCode: 50
|
||||
});
|
||||
t.equal(k.getKeyIsDown('@'), false);
|
||||
t.equal(k.getKeyIsDown('2'), false);
|
||||
t.equal(k.getKeyIsDown('any'), false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
133
scratch-vm/test/unit/io_mouse.js
Normal file
133
scratch-vm/test/unit/io_mouse.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const test = require('tap').test;
|
||||
const Mouse = require('../../src/io/mouse');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
t.type(m, 'object');
|
||||
t.type(m.postData, 'function');
|
||||
t.type(m.getClientX, 'function');
|
||||
t.type(m.getClientY, 'function');
|
||||
t.type(m.getScratchX, 'function');
|
||||
t.type(m.getScratchY, 'function');
|
||||
t.type(m.getIsDown, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mouseUp', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
m.postData({
|
||||
x: -20,
|
||||
y: 10,
|
||||
isDown: false,
|
||||
canvasWidth: 480,
|
||||
canvasHeight: 360
|
||||
});
|
||||
t.strictEquals(m.getClientX(), -20);
|
||||
t.strictEquals(m.getClientY(), 10);
|
||||
t.strictEquals(m.getScratchX(), -240);
|
||||
t.strictEquals(m.getScratchY(), 170);
|
||||
t.strictEquals(m.getIsDown(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mouseDown', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
m.postData({
|
||||
x: 9.9,
|
||||
y: 400.1,
|
||||
isDown: true,
|
||||
canvasWidth: 480,
|
||||
canvasHeight: 360
|
||||
});
|
||||
t.strictEquals(m.getClientX(), 9.9);
|
||||
t.strictEquals(m.getClientY(), 400.1);
|
||||
t.strictEquals(m.getScratchX(), -230);
|
||||
t.strictEquals(m.getScratchY(), -180);
|
||||
t.strictEquals(m.getIsDown(), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('at zoomed scale', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
m.postData({
|
||||
x: 240,
|
||||
y: 540,
|
||||
canvasWidth: 960,
|
||||
canvasHeight: 720
|
||||
});
|
||||
t.strictEquals(m.getClientX(), 240);
|
||||
t.strictEquals(m.getClientY(), 540);
|
||||
t.strictEquals(m.getScratchX(), -120);
|
||||
t.strictEquals(m.getScratchY(), -90);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mousedown activating click hats', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
const mouseMoveEvent = {
|
||||
x: 10,
|
||||
y: 100,
|
||||
canvasWidth: 480,
|
||||
canvasHeight: 360
|
||||
};
|
||||
|
||||
const dummyTarget = {
|
||||
draggable: false
|
||||
};
|
||||
|
||||
const mouseDownEvent = Object.assign({}, mouseMoveEvent, {
|
||||
isDown: true
|
||||
});
|
||||
|
||||
const mouseUpEvent = Object.assign({}, mouseMoveEvent, {
|
||||
isDown: false
|
||||
});
|
||||
|
||||
// Stub activateClickHats and pick function for testing
|
||||
let ranClickHats = false;
|
||||
m._activateClickHats = () => {
|
||||
ranClickHats = true;
|
||||
};
|
||||
m._pickTarget = () => dummyTarget;
|
||||
|
||||
// Mouse move without mousedown
|
||||
m.postData(mouseMoveEvent);
|
||||
t.strictEquals(ranClickHats, false);
|
||||
|
||||
// Mouse down event triggers the hats if target is not draggable
|
||||
dummyTarget.draggable = false;
|
||||
m.postData(mouseDownEvent);
|
||||
t.strictEquals(ranClickHats, true);
|
||||
|
||||
// But another mouse move while down doesn't trigger
|
||||
ranClickHats = false;
|
||||
m.postData(mouseDownEvent);
|
||||
t.strictEquals(ranClickHats, false);
|
||||
|
||||
// And it does trigger on mouse up if target is draggable
|
||||
ranClickHats = false;
|
||||
dummyTarget.draggable = true;
|
||||
m.postData(mouseUpEvent);
|
||||
t.strictEquals(ranClickHats, true);
|
||||
|
||||
// And hats don't trigger if mouse down is outside canvas
|
||||
ranClickHats = false;
|
||||
m.postData(Object.assign({}, mouseDownEvent, {
|
||||
x: 50000,
|
||||
y: 50
|
||||
}));
|
||||
t.strictEquals(ranClickHats, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
148
scratch-vm/test/unit/io_mouse_tw.js
Normal file
148
scratch-vm/test/unit/io_mouse_tw.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const test = require('tap').test;
|
||||
const Mouse = require('../../src/io/mouse');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('position clamping', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
const BIG = 9999;
|
||||
m.postData({
|
||||
x: BIG,
|
||||
y: BIG,
|
||||
canvasWidth: 480,
|
||||
canvasHeight: 360
|
||||
});
|
||||
t.strictEquals(m.getClientX(), BIG);
|
||||
t.strictEquals(m.getClientY(), BIG);
|
||||
t.strictEquals(m.getScratchX(), 240);
|
||||
t.strictEquals(m.getScratchY(), -180);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mouseButtonDown', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
t.strictEquals(m.getButtonIsDown(0), false);
|
||||
t.strictEquals(m.getButtonIsDown(1), false);
|
||||
t.strictEquals(m.getButtonIsDown(2), false);
|
||||
m.postData({
|
||||
isDown: true,
|
||||
button: 0
|
||||
});
|
||||
t.strictEquals(m.getButtonIsDown(0), true);
|
||||
t.strictEquals(m.getButtonIsDown(1), false);
|
||||
t.strictEquals(m.getButtonIsDown(2), false);
|
||||
m.postData({
|
||||
isDown: true,
|
||||
button: 2
|
||||
});
|
||||
t.strictEquals(m.getButtonIsDown(0), true);
|
||||
t.strictEquals(m.getButtonIsDown(1), false);
|
||||
t.strictEquals(m.getButtonIsDown(2), true);
|
||||
m.postData({
|
||||
isDown: false,
|
||||
button: 2
|
||||
});
|
||||
t.strictEquals(m.getButtonIsDown(0), true);
|
||||
t.strictEquals(m.getButtonIsDown(1), false);
|
||||
t.strictEquals(m.getButtonIsDown(2), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mouseDown with buttons', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
t.strictEquals(m.getIsDown(), false);
|
||||
m.postData({
|
||||
isDown: true,
|
||||
button: 0
|
||||
});
|
||||
t.strictEquals(m.getIsDown(), true);
|
||||
m.postData({
|
||||
isDown: true,
|
||||
button: 2
|
||||
});
|
||||
t.strictEquals(m.getIsDown(), true);
|
||||
m.postData({
|
||||
isDown: false,
|
||||
button: 2
|
||||
});
|
||||
t.strictEquals(m.getIsDown(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('missing button is treated as left', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
t.strictEquals(m.getButtonIsDown(0), false);
|
||||
m.postData({
|
||||
isDown: true
|
||||
});
|
||||
t.strictEquals(m.getButtonIsDown(0), true);
|
||||
m.postData({
|
||||
isDown: false
|
||||
});
|
||||
t.strictEquals(m.getButtonIsDown(0), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('usesRightClickDown', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
t.strictEquals(m.usesRightClickDown, false);
|
||||
t.strictEquals(m.getButtonIsDown(2), false);
|
||||
t.strictEquals(m.usesRightClickDown, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('no rounding when misc limits disabled', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
m.postData({
|
||||
x: 241,
|
||||
y: 541,
|
||||
canvasWidth: 960,
|
||||
canvasHeight: 720
|
||||
});
|
||||
t.equal(m.getScratchX(), -119);
|
||||
t.equal(m.getScratchY(), -90);
|
||||
|
||||
rt.setRuntimeOptions({
|
||||
miscLimits: false
|
||||
});
|
||||
t.equal(m.getScratchX(), -119.5);
|
||||
t.equal(m.getScratchY(), -90.5);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('accepts 0 as x and y position', t => {
|
||||
const rt = new Runtime();
|
||||
const m = new Mouse(rt);
|
||||
|
||||
m.postData({
|
||||
x: 1,
|
||||
y: 2,
|
||||
canvasWidth: 480,
|
||||
canvasHeight: 360
|
||||
});
|
||||
t.equal(m.getClientX(), 1);
|
||||
t.equal(m.getClientY(), 2);
|
||||
|
||||
m.postData({
|
||||
x: 0,
|
||||
y: 0,
|
||||
canvasWidth: 480,
|
||||
canvasHeight: 360
|
||||
});
|
||||
t.equal(m.getClientX(), 0);
|
||||
t.equal(m.getClientY(), 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
44
scratch-vm/test/unit/io_mousewheel.js
Normal file
44
scratch-vm/test/unit/io_mousewheel.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const test = require('tap').test;
|
||||
const MouseWheel = require('../../src/io/mouseWheel');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', t => {
|
||||
const rt = new Runtime();
|
||||
const mw = new MouseWheel(rt);
|
||||
|
||||
t.type(mw, 'object');
|
||||
t.type(mw.postData, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('blocks activated by scrolling', t => {
|
||||
let _startHatsArgs;
|
||||
const rt = {
|
||||
startHats: (...args) => {
|
||||
_startHatsArgs = args;
|
||||
}
|
||||
};
|
||||
const mw = new MouseWheel(rt);
|
||||
|
||||
_startHatsArgs = null;
|
||||
mw.postData({
|
||||
deltaY: -1
|
||||
});
|
||||
t.strictEquals(_startHatsArgs[0], 'event_whenkeypressed');
|
||||
t.strictEquals(_startHatsArgs[1].KEY_OPTION, 'up arrow');
|
||||
|
||||
_startHatsArgs = null;
|
||||
mw.postData({
|
||||
deltaY: +1
|
||||
});
|
||||
t.strictEquals(_startHatsArgs[0], 'event_whenkeypressed');
|
||||
t.strictEquals(_startHatsArgs[1].KEY_OPTION, 'down arrow');
|
||||
|
||||
_startHatsArgs = null;
|
||||
mw.postData({
|
||||
deltaY: 0
|
||||
});
|
||||
t.strictEquals(_startHatsArgs, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
26
scratch-vm/test/unit/io_scratchBLE.js
Normal file
26
scratch-vm/test/unit/io_scratchBLE.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const test = require('tap').test;
|
||||
// const ScratchBLE = require('../../src/io/scratchBLE');
|
||||
|
||||
test('constructor', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('waitForSocket', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('requestPeripheral', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('didReceiveCall', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('read', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('write', t => {
|
||||
t.end();
|
||||
});
|
||||
22
scratch-vm/test/unit/io_scratchBT.js
Normal file
22
scratch-vm/test/unit/io_scratchBT.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const test = require('tap').test;
|
||||
// const ScratchBT = require('../../src/io/scratchBT');
|
||||
|
||||
test('constructor', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('requestPeripheral', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('connectPeripheral', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('sendMessage', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('didReceiveCall', t => {
|
||||
t.end();
|
||||
});
|
||||
25
scratch-vm/test/unit/io_userData.js
Normal file
25
scratch-vm/test/unit/io_userData.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const test = require('tap').test;
|
||||
const UserData = require('../../src/io/userData');
|
||||
|
||||
test('spec', t => {
|
||||
const userData = new UserData();
|
||||
|
||||
t.type(userData, 'object');
|
||||
t.type(userData.postData, 'function');
|
||||
t.type(userData.getUsername, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getUsername returns empty string initially', t => {
|
||||
const userData = new UserData();
|
||||
|
||||
t.strictEquals(userData.getUsername(), '');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('postData sets the username', t => {
|
||||
const userData = new UserData();
|
||||
userData.postData({username: 'TEST'});
|
||||
t.strictEquals(userData.getUsername(), 'TEST');
|
||||
t.end();
|
||||
});
|
||||
77
scratch-vm/test/unit/maybe_format_message.js
Normal file
77
scratch-vm/test/unit/maybe_format_message.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const test = require('tap').test;
|
||||
const maybeFormatMessage = require('../../src/util/maybe-format-message');
|
||||
|
||||
const nonMessages = [
|
||||
'hi',
|
||||
42,
|
||||
true,
|
||||
function () {
|
||||
return 'unused';
|
||||
},
|
||||
{
|
||||
a: 1,
|
||||
b: 2
|
||||
},
|
||||
{
|
||||
id: 'almost a message',
|
||||
notDefault: 'but missing the "default" property'
|
||||
},
|
||||
{
|
||||
notId: 'this one is missing the "id" property',
|
||||
default: 'but has "default"'
|
||||
}
|
||||
];
|
||||
|
||||
const argsQuick = {
|
||||
speed: 'quick'
|
||||
};
|
||||
|
||||
const argsOther = {
|
||||
speed: 'slow'
|
||||
};
|
||||
|
||||
const argsEmpty = {};
|
||||
|
||||
const simpleMessage = {
|
||||
id: 'test.simpleMessage',
|
||||
default: 'The quick brown fox jumped over the lazy dog.'
|
||||
};
|
||||
|
||||
const complexMessage = {
|
||||
id: 'test.complexMessage',
|
||||
default: '{speed, select, quick {The quick brown fox jumped over the lazy dog.} other {Too slow, Gobo!}}'
|
||||
};
|
||||
|
||||
const quickExpectedResult = 'The quick brown fox jumped over the lazy dog.';
|
||||
const otherExpectedResult = 'Too slow, Gobo!';
|
||||
|
||||
test('preserve non-messages', t => {
|
||||
t.plan(nonMessages.length);
|
||||
|
||||
for (const x of nonMessages) {
|
||||
const result = maybeFormatMessage(x);
|
||||
t.strictSame(x, result);
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('format messages', t => {
|
||||
const quickResult1 = maybeFormatMessage(simpleMessage);
|
||||
t.strictNotSame(quickResult1, simpleMessage);
|
||||
t.same(quickResult1, quickExpectedResult);
|
||||
|
||||
const quickResult2 = maybeFormatMessage(complexMessage, argsQuick);
|
||||
t.strictNotSame(quickResult2, complexMessage);
|
||||
t.same(quickResult2, quickExpectedResult);
|
||||
|
||||
const otherResult1 = maybeFormatMessage(complexMessage, argsOther);
|
||||
t.strictNotSame(otherResult1, complexMessage);
|
||||
t.same(otherResult1, otherExpectedResult);
|
||||
|
||||
const otherResult2 = maybeFormatMessage(complexMessage, argsEmpty);
|
||||
t.strictNotSame(otherResult2, complexMessage);
|
||||
t.same(otherResult2, otherExpectedResult);
|
||||
|
||||
t.end();
|
||||
});
|
||||
91
scratch-vm/test/unit/mock-timer.js
Normal file
91
scratch-vm/test/unit/mock-timer.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const test = require('tap').test;
|
||||
const MockTimer = require('../fixtures/mock-timer');
|
||||
|
||||
test('spec', t => {
|
||||
const timer = new MockTimer();
|
||||
|
||||
t.type(MockTimer, 'function');
|
||||
t.type(timer, 'object');
|
||||
|
||||
// Most members of MockTimer mimic members of Timer.
|
||||
t.type(timer.startTime, 'number');
|
||||
t.type(timer.time, 'function');
|
||||
t.type(timer.start, 'function');
|
||||
t.type(timer.timeElapsed, 'function');
|
||||
t.type(timer.setTimeout, 'function');
|
||||
t.type(timer.clearTimeout, 'function');
|
||||
|
||||
// A few members of MockTimer have no Timer equivalent and should only be used in tests.
|
||||
t.type(timer.advanceMockTime, 'function');
|
||||
t.type(timer.advanceMockTimeAsync, 'function');
|
||||
t.type(timer.hasTimeouts, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('time', t => {
|
||||
const timer = new MockTimer();
|
||||
const delta = 1;
|
||||
|
||||
const time1 = timer.time();
|
||||
const time2 = timer.time();
|
||||
timer.advanceMockTime(delta);
|
||||
const time3 = timer.time();
|
||||
|
||||
t.equal(time1, time2);
|
||||
t.equal(time2 + delta, time3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('start / timeElapsed', t => new Promise(resolve => {
|
||||
const timer = new MockTimer();
|
||||
const halfDelay = 1;
|
||||
const fullDelay = halfDelay + halfDelay;
|
||||
|
||||
timer.start();
|
||||
|
||||
let timeoutCalled = 0;
|
||||
|
||||
// Wait and measure timer
|
||||
timer.setTimeout(() => {
|
||||
t.equal(timeoutCalled, 0);
|
||||
++timeoutCalled;
|
||||
|
||||
const timeElapsed = timer.timeElapsed();
|
||||
t.equal(timeElapsed, fullDelay);
|
||||
t.end();
|
||||
|
||||
resolve();
|
||||
}, fullDelay);
|
||||
|
||||
// this should not trigger the callback
|
||||
timer.advanceMockTime(halfDelay);
|
||||
|
||||
// give the mock timer a chance to run tasks
|
||||
global.setTimeout(() => {
|
||||
// we've only mock-waited for half the delay so it should not have run yet
|
||||
t.equal(timeoutCalled, 0);
|
||||
|
||||
// this should trigger the callback
|
||||
timer.advanceMockTime(halfDelay);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
test('clearTimeout / hasTimeouts', t => new Promise((resolve, reject) => {
|
||||
const timer = new MockTimer();
|
||||
|
||||
const timeoutId = timer.setTimeout(() => {
|
||||
reject(new Error('Canceled task ran'));
|
||||
}, 1);
|
||||
|
||||
timer.setTimeout(() => {
|
||||
resolve('Non-canceled task ran');
|
||||
t.end();
|
||||
}, 2);
|
||||
|
||||
timer.clearTimeout(timeoutId);
|
||||
|
||||
while (timer.hasTimeouts()) {
|
||||
timer.advanceMockTime(1);
|
||||
}
|
||||
}));
|
||||
240
scratch-vm/test/unit/project_changed_state.js
Normal file
240
scratch-vm/test/unit/project_changed_state.js
Normal file
@@ -0,0 +1,240 @@
|
||||
const tap = require('tap');
|
||||
const path = require('path');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
let vm;
|
||||
let projectChanged;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
vm = new VirtualMachine();
|
||||
|
||||
vm.runtime.addListener('PROJECT_CHANGED', () => {
|
||||
projectChanged = true;
|
||||
});
|
||||
|
||||
vm.attachStorage(makeTestStorage());
|
||||
return vm.loadProject(project).then(() => {
|
||||
// The test in project_load_changed_state.js tests
|
||||
// that loading a project does not emit a project changed
|
||||
// event. This setup tries to be agnostic of whether that
|
||||
// test is passing or failing.
|
||||
projectChanged = false;
|
||||
});
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('Adding a sprite (from sprite2) should emit a project changed event', t => {
|
||||
const sprite2Uri = path.resolve(__dirname, '../fixtures/cat.sprite2');
|
||||
const sprite2 = readFileToBuffer(sprite2Uri);
|
||||
|
||||
vm.addSprite(sprite2).then(() => {
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Adding a sprite (from sprite3) should emit a project changed event', t => {
|
||||
const sprite3Uri = path.resolve(__dirname, '../fixtures/cat.sprite3');
|
||||
const sprite3 = readFileToBuffer(sprite3Uri);
|
||||
|
||||
vm.addSprite(sprite3).then(() => {
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Adding a costume should emit a project changed event', t => {
|
||||
const newCostume = {
|
||||
name: 'costume1',
|
||||
baseLayerID: 0,
|
||||
baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||
bitmapResolution: 1,
|
||||
rotationCenterX: 47,
|
||||
rotationCenterY: 55
|
||||
};
|
||||
|
||||
vm.addCostume('f9a1c175dbe2e5dee472858dd30d16bb.svg', newCostume).then(() => {
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Adding a costume from library should emit a project changed event', t => {
|
||||
const newCostume = {
|
||||
name: 'costume1',
|
||||
baseLayerID: 0,
|
||||
baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||
bitmapResolution: 1,
|
||||
rotationCenterX: 47,
|
||||
rotationCenterY: 55
|
||||
};
|
||||
|
||||
vm.addCostumeFromLibrary('f9a1c175dbe2e5dee472858dd30d16bb.svg', newCostume).then(() => {
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Adding a backdrop should emit a project changed event', t => {
|
||||
const newCostume = {
|
||||
name: 'costume1',
|
||||
baseLayerID: 0,
|
||||
baseLayerMD5: 'f9a1c175dbe2e5dee472858dd30d16bb.svg',
|
||||
bitmapResolution: 1,
|
||||
rotationCenterX: 47,
|
||||
rotationCenterY: 55
|
||||
};
|
||||
|
||||
vm.addBackdrop('f9a1c175dbe2e5dee472858dd30d16bb.svg', newCostume).then(() => {
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Adding a sound should emit a project changed event', t => {
|
||||
const newSound = {
|
||||
soundName: 'meow',
|
||||
soundID: 0,
|
||||
md5: '83c36d806dc92327b9e7049a565c6bff.wav',
|
||||
sampleCount: 18688,
|
||||
rate: 22050
|
||||
};
|
||||
|
||||
vm.addSound(newSound).then(() => {
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Deleting a sprite should emit a project changed event', t => {
|
||||
const spriteId = vm.editingTarget.id;
|
||||
|
||||
vm.deleteSprite(spriteId);
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a costume should emit a project changed event', t => {
|
||||
vm.deleteCostume(0);
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a sound should emit a project changed event', t => {
|
||||
vm.deleteSound(0);
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Reordering a sprite should emit a project changed event', t => {
|
||||
const sprite3Uri = path.resolve(__dirname, '../fixtures/cat.sprite3');
|
||||
const sprite3 = readFileToBuffer(sprite3Uri);
|
||||
|
||||
// Add a new sprite so we have 2 to reorder
|
||||
vm.addSprite(sprite3).then(() => {
|
||||
// Reset the project changed flag to ignore change from adding new sprite
|
||||
projectChanged = false;
|
||||
t.equal(vm.runtime.targets.filter(target => !target.isStage).length, 2);
|
||||
vm.reorderTarget(2, 1);
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Reordering a costume should emit a project changed event', t => {
|
||||
t.equal(vm.editingTarget.sprite.costumes.length, 2);
|
||||
const spriteId = vm.editingTarget.id;
|
||||
const reordered = vm.reorderCostume(spriteId, 1, 0);
|
||||
t.equal(reordered, true);
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Reordering a sound should emit a project changed event', t => {
|
||||
const spriteId = vm.editingTarget.id;
|
||||
const newSound = {
|
||||
soundName: 'meow',
|
||||
soundID: 0,
|
||||
md5: '83c36d806dc92327b9e7049a565c6bff.wav',
|
||||
sampleCount: 18688,
|
||||
rate: 22050
|
||||
};
|
||||
vm.addSound(newSound).then(() => {
|
||||
// Reset the project changed flag to ignore change from adding new sound
|
||||
projectChanged = false;
|
||||
t.equal(vm.editingTarget.sprite.sounds.length, 2);
|
||||
const reordered = vm.reorderSound(spriteId, 1, 0);
|
||||
t.equal(reordered, true);
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Renaming a sprite should emit a project changed event', t => {
|
||||
const spriteId = vm.editingTarget.id;
|
||||
vm.renameSprite(spriteId, 'My Sprite');
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Renaming a costume should emit a project changed event', t => {
|
||||
vm.renameCostume(0, 'My Costume');
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Renaming a sound should emit a project changed event', t => {
|
||||
vm.renameSound(0, 'My Sound');
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Changing sprite info should emit a project changed event', t => {
|
||||
const newSpritePosition = {
|
||||
x: 10,
|
||||
y: 100
|
||||
};
|
||||
|
||||
vm.postSpriteInfo(newSpritePosition);
|
||||
t.equal(projectChanged, true);
|
||||
projectChanged = false;
|
||||
|
||||
const newSpriteDirection = {
|
||||
direction: -30
|
||||
};
|
||||
|
||||
vm.postSpriteInfo(newSpriteDirection);
|
||||
t.equal(projectChanged, true);
|
||||
projectChanged = false;
|
||||
|
||||
t.end();
|
||||
|
||||
});
|
||||
|
||||
test('Editing a vector costume should emit a project changed event', t => {
|
||||
const mockSvg = 'svg';
|
||||
const mockRotationX = -13;
|
||||
const mockRotationY = 25;
|
||||
|
||||
vm.updateSvg(0, mockSvg, mockRotationX, mockRotationY);
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Editing a sound should emit a project changed event', t => {
|
||||
const mockSoundBuffer = [];
|
||||
const mockSoundEncoding = [];
|
||||
|
||||
vm.updateSoundBuffer(0, mockSoundBuffer, mockSoundEncoding);
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
463
scratch-vm/test/unit/project_changed_state_blocks.js
Normal file
463
scratch-vm/test/unit/project_changed_state_blocks.js
Normal file
@@ -0,0 +1,463 @@
|
||||
const tap = require('tap');
|
||||
const path = require('path');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
let vm;
|
||||
let projectChanged;
|
||||
let blockContainer;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
vm = new VirtualMachine();
|
||||
|
||||
vm.runtime.addListener('PROJECT_CHANGED', () => {
|
||||
projectChanged = true;
|
||||
});
|
||||
|
||||
vm.attachStorage(makeTestStorage());
|
||||
return vm.loadProject(project).then(() => {
|
||||
blockContainer = vm.editingTarget.blocks;
|
||||
|
||||
// Add mock blocks to use for tests
|
||||
blockContainer.createBlock({
|
||||
id: 'a parent block',
|
||||
opcode: 'my_testParentBlock',
|
||||
fields: {},
|
||||
inputs: {}
|
||||
});
|
||||
|
||||
|
||||
blockContainer.createBlock({
|
||||
id: 'a new block',
|
||||
opcode: 'my_testBlock',
|
||||
topLevel: true,
|
||||
x: -10,
|
||||
y: 35,
|
||||
fields: {
|
||||
A_FIELD: {
|
||||
name: 'A_FIELD',
|
||||
value: 10
|
||||
}
|
||||
},
|
||||
inputs: {},
|
||||
parent: 'a block'
|
||||
});
|
||||
|
||||
// Reset project changes from new blocks
|
||||
projectChanged = false;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('Creating a block should emit a project changed event', t => {
|
||||
blockContainer.createBlock({
|
||||
id: 'another block',
|
||||
opcode: 'my_testBlock',
|
||||
topLevel: true
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a block should emit a project changed event', t => {
|
||||
blockContainer.deleteBlock('a new block');
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Changing a block should emit a project changed event', t => {
|
||||
blockContainer.changeBlock({
|
||||
element: 'field',
|
||||
id: 'a new block',
|
||||
name: 'A_FIELD',
|
||||
value: 300
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.changeBlock({
|
||||
element: 'checkbox',
|
||||
id: 'a new block',
|
||||
value: true
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.changeBlock({
|
||||
element: 'mutation',
|
||||
id: 'a new block',
|
||||
value: '<mutation></mutation>'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Moving a block to a new position should emit a project changed event', t => {
|
||||
blockContainer.moveBlock({
|
||||
id: 'a new block',
|
||||
newCoordinate: {
|
||||
x: -40,
|
||||
y: 350
|
||||
}
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Connecting a block to a new parent should emit a project changed event', t => {
|
||||
blockContainer.createBlock({
|
||||
id: 'another block',
|
||||
opcode: 'my_testBlock'
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.moveBlock({
|
||||
id: 'a new block',
|
||||
newParent: 'another block'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Disconnecting a block from another should emit a project changed event', t => {
|
||||
blockContainer.moveBlock({
|
||||
id: 'a new block',
|
||||
oldParent: 'a parent block'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Creating a local variable should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_create',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: true,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
// Creating the same variable twice should not emit a project changed event
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_create',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: true,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
t.equal(projectChanged, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Creating a global variable should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_create',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: false,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
// Creating the same variable twice should not emit a project changed event
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_create',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: false,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
t.equal(projectChanged, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Renaming a variable should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_create',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: false,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_rename',
|
||||
varId: 'a new variable',
|
||||
oldName: 'foo',
|
||||
newName: 'bar'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a variable should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_create',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: false,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.blocklyListen({
|
||||
type: 'var_delete',
|
||||
varId: 'a new variable',
|
||||
varName: 'foo',
|
||||
varType: '',
|
||||
isLocal: false,
|
||||
isCloud: false
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Creating a block comment should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_create',
|
||||
blockId: 'a new block',
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Creating a workspace comment should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_create',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Changing a comment should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_create',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_change',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
newContents_: {
|
||||
minimized: true
|
||||
},
|
||||
oldContents_: {
|
||||
minimized: false
|
||||
}
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Attempting to change a comment that does not exist should not emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_change',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
newContents_: {
|
||||
minimized: true
|
||||
},
|
||||
oldContents_: {
|
||||
minimized: false
|
||||
}
|
||||
});
|
||||
|
||||
t.equal(projectChanged, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a block comment should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_create',
|
||||
blockId: 'a new block',
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_delete',
|
||||
blockId: 'a new block',
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a workspace comment should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_create',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_delete',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Deleting a comment that does not exist should not emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_delete',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
t.equal(projectChanged, false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Moving a comment should emit a project changed event', t => {
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_create',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
height: 250,
|
||||
width: 400,
|
||||
xy: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
minimized: false,
|
||||
text: 'comment'
|
||||
});
|
||||
|
||||
projectChanged = false;
|
||||
|
||||
blockContainer.blocklyListen({
|
||||
type: 'comment_move',
|
||||
blockId: null,
|
||||
commentId: 'a new comment',
|
||||
oldCoordinate_: {
|
||||
x: -40,
|
||||
y: 27
|
||||
},
|
||||
newCoordinate_: {
|
||||
x: -35,
|
||||
y: 50
|
||||
}
|
||||
});
|
||||
|
||||
t.equal(projectChanged, true);
|
||||
t.end();
|
||||
});
|
||||
28
scratch-vm/test/unit/project_load_changed_state.js
Normal file
28
scratch-vm/test/unit/project_load_changed_state.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const tap = require('tap');
|
||||
const path = require('path');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
// Test that loading a project does not emit a project change
|
||||
// This is in its own file so that it does not affect the test setup
|
||||
// and results of the other project changed state tests
|
||||
test('Loading a project should not emit a project changed event', t => {
|
||||
const projectUri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const project = readFileToBuffer(projectUri);
|
||||
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
let projectChanged = false;
|
||||
vm.runtime.addListener('PROJECT_CHANGED', () => {
|
||||
projectChanged = true;
|
||||
});
|
||||
|
||||
vm.attachStorage(makeTestStorage());
|
||||
return vm.loadProject(project).then(() => {
|
||||
t.equal(projectChanged, false);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
104
scratch-vm/test/unit/serialization_sb2.js
Normal file
104
scratch-vm/test/unit/serialization_sb2.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const extractProjectJson = require('../fixtures/readProjectFile').extractProjectJson;
|
||||
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const sb2 = require('../../src/serialization/sb2');
|
||||
|
||||
test('spec', t => {
|
||||
t.type(sb2, 'object');
|
||||
t.type(sb2.deserialize, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('default', t => {
|
||||
// Get SB2 JSON (string)
|
||||
const uri = path.resolve(__dirname, '../fixtures/default.sb2');
|
||||
const json = extractProjectJson(uri);
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new Runtime();
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
// Test
|
||||
t.type(json, 'object');
|
||||
t.type(rt, 'object');
|
||||
t.type(targets, 'object');
|
||||
|
||||
t.ok(targets[0] instanceof RenderedTarget);
|
||||
t.type(targets[0].id, 'string');
|
||||
t.type(targets[0].blocks, 'object');
|
||||
t.type(targets[0].variables, 'object');
|
||||
t.type(targets[0].comments, 'object');
|
||||
|
||||
t.equal(targets[0].isOriginal, true);
|
||||
t.equal(targets[0].currentCostume, 0);
|
||||
t.equal(targets[0].isOriginal, true);
|
||||
t.equal(targets[0].isStage, true);
|
||||
|
||||
t.ok(targets[1] instanceof RenderedTarget);
|
||||
t.type(targets[1].id, 'string');
|
||||
t.type(targets[1].blocks, 'object');
|
||||
t.type(targets[1].variables, 'object');
|
||||
t.type(targets[1].comments, 'object');
|
||||
|
||||
t.equal(targets[1].isOriginal, true);
|
||||
t.equal(targets[1].currentCostume, 0);
|
||||
t.equal(targets[1].isOriginal, true);
|
||||
t.equal(targets[1].isStage, false);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('data scoping', t => {
|
||||
// Get SB2 JSON (string)
|
||||
const uri = path.resolve(__dirname, '../fixtures/data.sb2');
|
||||
const json = extractProjectJson(uri);
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new Runtime();
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
const globalVariableIds = Object.keys(targets[0].variables);
|
||||
const localVariableIds = Object.keys(targets[1].variables);
|
||||
t.equal(targets[0].variables[globalVariableIds[0]].name, 'foo');
|
||||
t.equal(targets[1].variables[localVariableIds[0]].name, 'local');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('whenclicked blocks imported separately', t => {
|
||||
// This sb2 fixture has a single "whenClicked" block on both sprite and stage
|
||||
const uri = path.resolve(__dirname, '../fixtures/when-clicked.sb2');
|
||||
const json = extractProjectJson(uri);
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new Runtime();
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
const stage = targets[0];
|
||||
t.equal(stage.isStage, true); // Make sure we have the correct target
|
||||
const stageOpcode = stage.blocks.getBlock(stage.blocks.getScripts()[0]).opcode;
|
||||
t.equal(stageOpcode, 'event_whenstageclicked');
|
||||
|
||||
const sprite = targets[1];
|
||||
t.equal(sprite.isStage, false); // Make sure we have the correct target
|
||||
const spriteOpcode = sprite.blocks.getBlock(sprite.blocks.getScripts()[0]).opcode;
|
||||
t.equal(spriteOpcode, 'event_whenthisspriteclicked');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Ordering', t => {
|
||||
// This SB2 has 3 sprites that have been reordered in scratch 2
|
||||
// so the order in the file is not the order specified by the indexInLibrary property.
|
||||
const uri = path.resolve(__dirname, '../fixtures/ordering.sb2');
|
||||
const json = extractProjectJson(uri);
|
||||
const rt = new Runtime();
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
// Would fail with any other ordering.
|
||||
t.equal(targets[1].sprite.name, 'First');
|
||||
t.equal(targets[2].sprite.name, 'Second');
|
||||
t.equal(targets[3].sprite.name, 'Third');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
363
scratch-vm/test/unit/serialization_sb3.js
Normal file
363
scratch-vm/test/unit/serialization_sb3.js
Normal file
@@ -0,0 +1,363 @@
|
||||
const test = require('tap').test;
|
||||
const path = require('path');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const sb3 = require('../../src/serialization/sb3');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const exampleProjectPath = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2');
|
||||
const commentsSB2ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb2');
|
||||
const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3');
|
||||
const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3');
|
||||
const variableReporterSB2ProjectPath = path.resolve(__dirname, '../fixtures/top-level-variable-reporter.sb2');
|
||||
const topLevelReportersProjectPath = path.resolve(__dirname, '../fixtures/top-level-reporters.sb3');
|
||||
const draggableSB3ProjectPath = path.resolve(__dirname, '../fixtures/draggable.sb3');
|
||||
const originSB3ProjectPath = path.resolve(__dirname, '../fixtures/origin.sb3');
|
||||
const originAbsentSB3ProjectPath = path.resolve(__dirname, '../fixtures/origin-absent.sb3');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
|
||||
test('serialize', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(exampleProjectPath))
|
||||
.then(() => {
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
// @todo Analyze
|
||||
t.type(JSON.stringify(result), 'string');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize', t => {
|
||||
const vm = new VirtualMachine();
|
||||
sb3.deserialize('', vm.runtime).then(({targets}) => {
|
||||
// @todo Analyze
|
||||
t.type(targets, 'object');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('serialize sb2 project with comments as sb3', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(commentsSB2ProjectPath))
|
||||
.then(() => {
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
|
||||
t.type(JSON.stringify(result), 'string');
|
||||
t.type(result.targets, 'object');
|
||||
t.equal(Array.isArray(result.targets), true);
|
||||
t.equal(result.targets.length, 2);
|
||||
|
||||
const stage = result.targets[0];
|
||||
t.equal(stage.isStage, true);
|
||||
// The stage has 0 blocks, and 1 workspace comment
|
||||
t.type(stage.blocks, 'object');
|
||||
t.equal(Object.keys(stage.blocks).length, 0);
|
||||
t.type(stage.comments, 'object');
|
||||
t.equal(Object.keys(stage.comments).length, 1);
|
||||
const stageBlockComments = Object.values(stage.comments).filter(comment => !!comment.blockId);
|
||||
const stageWorkspaceComments = Object.values(stage.comments).filter(comment => comment.blockId === null);
|
||||
t.equal(stageBlockComments.length, 0);
|
||||
t.equal(stageWorkspaceComments.length, 1);
|
||||
|
||||
const sprite = result.targets[1];
|
||||
t.equal(sprite.isStage, false);
|
||||
t.type(sprite.blocks, 'object');
|
||||
// Sprite 1 has 6 blocks, 5 block comments, and 1 workspace comment
|
||||
t.equal(Object.keys(sprite.blocks).length, 6);
|
||||
t.type(sprite.comments, 'object');
|
||||
t.equal(Object.keys(sprite.comments).length, 6);
|
||||
|
||||
const spriteBlockComments = Object.values(sprite.comments).filter(comment => !!comment.blockId);
|
||||
const spriteWorkspaceComments = Object.values(sprite.comments).filter(comment => comment.blockId === null);
|
||||
t.equal(spriteBlockComments.length, 5);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize sb3 project with comments', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(commentsSB3ProjectPath))
|
||||
.then(() => {
|
||||
const runtime = vm.runtime;
|
||||
|
||||
t.type(runtime.targets, 'object');
|
||||
t.equal(Array.isArray(runtime.targets), true);
|
||||
t.equal(runtime.targets.length, 2);
|
||||
|
||||
const stage = runtime.targets[0];
|
||||
t.equal(stage.isStage, true);
|
||||
// The stage has 0 blocks, and 1 workspace comment
|
||||
t.type(stage.blocks, 'object');
|
||||
t.equal(Object.keys(stage.blocks._blocks).length, 0);
|
||||
t.type(stage.comments, 'object');
|
||||
t.equal(Object.keys(stage.comments).length, 1);
|
||||
const stageBlockComments = Object.values(stage.comments).filter(comment => !!comment.blockId);
|
||||
const stageWorkspaceComments = Object.values(stage.comments).filter(comment => comment.blockId === null);
|
||||
t.equal(stageBlockComments.length, 0);
|
||||
t.equal(stageWorkspaceComments.length, 1);
|
||||
|
||||
const sprite = runtime.targets[1];
|
||||
t.equal(sprite.isStage, false);
|
||||
t.type(sprite.blocks, 'object');
|
||||
// Sprite 1 has 6 blocks, 5 block comments, and 1 workspace comment
|
||||
t.equal(Object.values(sprite.blocks._blocks).filter(block => !block.shadow).length, 6);
|
||||
t.type(sprite.comments, 'object');
|
||||
t.equal(Object.keys(sprite.comments).length, 6);
|
||||
|
||||
const spriteBlockComments = Object.values(sprite.comments).filter(comment => !!comment.blockId);
|
||||
const spriteWorkspaceComments = Object.values(sprite.comments).filter(comment => comment.blockId === null);
|
||||
t.equal(spriteBlockComments.length, 5);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserialize sb3 project with comments - no duplicate id serialization', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(commentsSB3NoDupeIds))
|
||||
.then(() => {
|
||||
const runtime = vm.runtime;
|
||||
|
||||
t.type(runtime.targets, 'object');
|
||||
t.equal(Array.isArray(runtime.targets), true);
|
||||
t.equal(runtime.targets.length, 2);
|
||||
|
||||
const stage = runtime.targets[0];
|
||||
t.equal(stage.isStage, true);
|
||||
// The stage has 0 blocks, and 0 workspace comment
|
||||
t.type(stage.blocks, 'object');
|
||||
t.equal(Object.keys(stage.blocks._blocks).length, 0);
|
||||
t.type(stage.comments, 'object');
|
||||
t.equal(Object.keys(stage.comments).length, 0);
|
||||
|
||||
const sprite = runtime.targets[1];
|
||||
t.equal(sprite.isStage, false);
|
||||
t.type(sprite.blocks, 'object');
|
||||
// Sprite1 has 1 blocks, 1 block comment, and 1 workspace comment
|
||||
t.equal(Object.values(sprite.blocks._blocks).filter(block => !block.shadow).length, 1);
|
||||
t.type(sprite.comments, 'object');
|
||||
t.equal(Object.keys(sprite.comments).length, 2);
|
||||
|
||||
const spriteBlockComments = Object.values(sprite.comments).filter(comment => !!comment.blockId);
|
||||
const spriteWorkspaceComments = Object.values(sprite.comments).filter(comment => comment.blockId === null);
|
||||
t.equal(spriteBlockComments.length, 1);
|
||||
t.equal(spriteWorkspaceComments.length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('serializing and deserializing sb3 preserves sprite layer order', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
return vm.loadProject(readFileToBuffer(path.resolve(__dirname, '../fixtures/ordering.sb2')))
|
||||
.then(() => {
|
||||
// Target get layer order needs a renderer,
|
||||
// fake the numbers we would get back from the
|
||||
// renderer in order to test that they are serialized
|
||||
// correctly
|
||||
vm.runtime.targets[0].getLayerOrder = () => 0;
|
||||
vm.runtime.targets[1].getLayerOrder = () => 20;
|
||||
vm.runtime.targets[2].getLayerOrder = () => 10;
|
||||
vm.runtime.targets[3].getLayerOrder = () => 30;
|
||||
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
|
||||
t.type(JSON.stringify(result), 'string');
|
||||
t.type(result.targets, 'object');
|
||||
t.equal(Array.isArray(result.targets), true);
|
||||
t.equal(result.targets.length, 4);
|
||||
|
||||
// First check that the sprites are ordered correctly (as they would
|
||||
// appear in the target pane)
|
||||
t.equal(result.targets[0].name, 'Stage');
|
||||
t.equal(result.targets[1].name, 'First');
|
||||
t.equal(result.targets[2].name, 'Second');
|
||||
t.equal(result.targets[3].name, 'Third');
|
||||
|
||||
// Check that they are in the correct layer order (as they would render
|
||||
// back to front on the stage)
|
||||
t.equal(result.targets[0].layerOrder, 0);
|
||||
t.equal(result.targets[1].layerOrder, 2);
|
||||
t.equal(result.targets[2].layerOrder, 1);
|
||||
t.equal(result.targets[3].layerOrder, 3);
|
||||
|
||||
return result;
|
||||
})
|
||||
.then(serializedObject =>
|
||||
sb3.deserialize(
|
||||
JSON.parse(JSON.stringify(serializedObject)), new Runtime(), null, false)
|
||||
.then(({targets}) => {
|
||||
// First check that the sprites are ordered correctly (as they would
|
||||
// appear in the target pane)
|
||||
t.equal(targets[0].sprite.name, 'Stage');
|
||||
t.equal(targets[1].sprite.name, 'First');
|
||||
t.equal(targets[2].sprite.name, 'Second');
|
||||
t.equal(targets[3].sprite.name, 'Third');
|
||||
|
||||
// Check that they are in the correct layer order (as they would render
|
||||
// back to front on the stage)
|
||||
t.equal(targets[0].layerOrder, 0);
|
||||
t.equal(targets[1].layerOrder, 2);
|
||||
t.equal(targets[2].layerOrder, 1);
|
||||
t.equal(targets[3].layerOrder, 3);
|
||||
|
||||
t.end();
|
||||
}));
|
||||
});
|
||||
|
||||
test('serializeBlocks', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(commentsSB3ProjectPath))
|
||||
.then(() => {
|
||||
const blocks = vm.runtime.targets[1].blocks._blocks;
|
||||
const result = sb3.serializeBlocks(blocks);
|
||||
// @todo Analyze
|
||||
t.type(result[0], 'object');
|
||||
t.ok(Object.keys(result[0]).length < Object.keys(blocks).length, 'less blocks in serialized format');
|
||||
t.ok(Array.isArray(result[1]));
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('serializeBlocks serializes x and y for topLevel blocks with x,y of 0,0', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(topLevelReportersProjectPath))
|
||||
.then(() => {
|
||||
// Verify that there are 2 blocks and they are both top level
|
||||
const blocks = vm.runtime.targets[1].blocks._blocks;
|
||||
const blockIds = Object.keys(blocks);
|
||||
t.equal(blockIds.length, 2);
|
||||
const blocksArray = blockIds.map(key => blocks[key]);
|
||||
t.equal(blocksArray.every(b => b.topLevel), true);
|
||||
// Simulate cleaning up the blocks by resetting x and y positions to 0
|
||||
blockIds.forEach(blockId => {
|
||||
blocks[blockId].x = 0;
|
||||
blocks[blockId].y = 0;
|
||||
});
|
||||
const result = sb3.serializeBlocks(blocks);
|
||||
const serializedBlocks = result[0];
|
||||
|
||||
t.type(serializedBlocks, 'object');
|
||||
const serializedBlockIds = Object.keys(serializedBlocks);
|
||||
t.equal(serializedBlockIds.length, 2);
|
||||
const firstBlock = serializedBlocks[serializedBlockIds[0]];
|
||||
const secondBlock = serializedBlocks[serializedBlockIds[1]];
|
||||
t.equal(firstBlock.x, 0);
|
||||
t.equal(firstBlock.y, 0);
|
||||
t.equal(secondBlock.x, 0);
|
||||
t.equal(secondBlock.y, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserializeBlocks', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(commentsSB3ProjectPath))
|
||||
.then(() => {
|
||||
const blocks = vm.runtime.targets[1].blocks._blocks;
|
||||
const serialized = sb3.serializeBlocks(blocks)[0];
|
||||
const deserialized = sb3.deserializeBlocks(serialized);
|
||||
t.equal(Object.keys(deserialized).length, Object.keys(blocks).length, 'same number of blocks');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('deserializeBlocks on already deserialized input', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(commentsSB3ProjectPath))
|
||||
.then(() => {
|
||||
const blocks = vm.runtime.targets[1].blocks._blocks;
|
||||
const serialized = sb3.serializeBlocks(blocks)[0];
|
||||
const deserialized = sb3.deserializeBlocks(serialized);
|
||||
const deserializedAgain = sb3.deserializeBlocks(deserialized);
|
||||
t.deepEqual(deserialized, deserializedAgain, 'no change from second pass of deserialize');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('getExtensionIdForOpcode', t => {
|
||||
t.equal(sb3.getExtensionIdForOpcode('wedo_loopy'), 'wedo');
|
||||
|
||||
// does not consider CORE to be extensions
|
||||
t.false(sb3.getExtensionIdForOpcode('control_loopy'));
|
||||
|
||||
// only considers things before the first underscore
|
||||
t.equal(sb3.getExtensionIdForOpcode('hello_there_loopy'), 'hello');
|
||||
|
||||
// does not return anything for opcodes with no extension
|
||||
t.false(sb3.getExtensionIdForOpcode('hello'));
|
||||
|
||||
// forbidden characters must be replaced with '-'
|
||||
t.equal(sb3.getExtensionIdForOpcode('hi:there/happy_people'), 'hi-there-happy');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('(#1608) serializeBlocks maintains top level variable reporters', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(variableReporterSB2ProjectPath))
|
||||
.then(() => {
|
||||
const blocks = vm.runtime.targets[0].blocks._blocks;
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
// Project should have 1 block, a top-level variable reporter
|
||||
t.equal(Object.keys(blocks).length, 1);
|
||||
t.equal(Object.keys(result.targets[0].blocks).length, 1);
|
||||
|
||||
// Make sure deserializing these blocks works
|
||||
t.doesNotThrow(() => {
|
||||
sb3.deserialize(JSON.parse(JSON.stringify(result)), vm.runtime);
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('(#1850) sprite draggability state read when loading SB3 file', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(draggableSB3ProjectPath))
|
||||
.then(() => {
|
||||
const sprite1Obj = vm.runtime.targets.find(target => target.sprite.name === 'Sprite1');
|
||||
// Sprite1 in project should have draggable set to true
|
||||
t.equal(sprite1Obj.draggable, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('load origin value from SB3 file json metadata', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(originSB3ProjectPath))
|
||||
.then(() => {
|
||||
t.type(vm.runtime.origin, 'string');
|
||||
})
|
||||
.then(() => vm.loadProject(readFileToBuffer(originAbsentSB3ProjectPath)))
|
||||
.then(() => {
|
||||
// After loading a project with an origin, then loading one without an origin,
|
||||
// origin value should no longer be set.
|
||||
t.equal(vm.runtime.origin, null);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('serialize origin value if it is present', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(originSB3ProjectPath))
|
||||
.then(() => {
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
t.type(result.meta.origin, 'string');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('do not serialize origin value if it is not present', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.loadProject(readFileToBuffer(originAbsentSB3ProjectPath))
|
||||
.then(() => {
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
t.equal(result.meta.origin, undefined);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
36
scratch-vm/test/unit/spec.js
Normal file
36
scratch-vm/test/unit/spec.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const test = require('tap').test;
|
||||
const VirtualMachine = require('../../src/index');
|
||||
|
||||
test('interface', t => {
|
||||
const vm = new VirtualMachine();
|
||||
t.type(vm, 'object');
|
||||
t.type(vm.start, 'function');
|
||||
t.type(vm.greenFlag, 'function');
|
||||
t.type(vm.setTurboMode, 'function');
|
||||
t.type(vm.setCompatibilityMode, 'function');
|
||||
t.type(vm.stopAll, 'function');
|
||||
t.type(vm.clear, 'function');
|
||||
|
||||
t.type(vm.getPlaygroundData, 'function');
|
||||
t.type(vm.postIOData, 'function');
|
||||
|
||||
t.type(vm.loadProject, 'function');
|
||||
t.type(vm.addSprite, 'function');
|
||||
t.type(vm.addCostume, 'function');
|
||||
t.type(vm.addBackdrop, 'function');
|
||||
t.type(vm.addSound, 'function');
|
||||
t.type(vm.deleteCostume, 'function');
|
||||
t.type(vm.deleteSound, 'function');
|
||||
t.type(vm.renameSprite, 'function');
|
||||
t.type(vm.deleteSprite, 'function');
|
||||
|
||||
t.type(vm.attachRenderer, 'function');
|
||||
t.type(vm.blockListener, 'function');
|
||||
t.type(vm.flyoutBlockListener, 'function');
|
||||
t.type(vm.setEditingTarget, 'function');
|
||||
|
||||
t.type(vm.emitTargetsUpdate, 'function');
|
||||
t.type(vm.emitWorkspaceUpdate, 'function');
|
||||
t.type(vm.postSpriteInfo, 'function');
|
||||
t.end();
|
||||
});
|
||||
585
scratch-vm/test/unit/sprites_rendered-target.js
Normal file
585
scratch-vm/test/unit/sprites_rendered-target.js
Normal file
@@ -0,0 +1,585 @@
|
||||
const test = require('tap').test;
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
|
||||
test('clone effects', t => {
|
||||
// Create two clones and ensure they have different graphic effect objects.
|
||||
// Regression test for Github issue #224
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
const b = new RenderedTarget(spr, r);
|
||||
t.ok(a.effects !== b.effects);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setxy', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
a.setXY(123, 321, true);
|
||||
t.equals(a.x, 123);
|
||||
t.equals(a.y, 321);
|
||||
renderer.getFencedPositionOfDrawable = () => [50, 50];
|
||||
a.setXY(100, 100, true);
|
||||
t.equals(a.x, 50);
|
||||
t.equals(a.y, 50);
|
||||
r.setRuntimeOptions({
|
||||
fencing: false
|
||||
});
|
||||
a.setXY(100, 100, true);
|
||||
t.equals(a.x, 100);
|
||||
t.equals(a.y, 100);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('blocks get new id on duplicate', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const rt = new RenderedTarget(s, r);
|
||||
const block = {
|
||||
id: 'id1',
|
||||
topLevel: true,
|
||||
fields: {}
|
||||
};
|
||||
|
||||
rt.blocks.createBlock(block);
|
||||
|
||||
return rt.duplicate().then(duplicate => {
|
||||
t.notOk(Object.prototype.hasOwnProperty.call(duplicate.blocks._blocks, block.id));
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('direction', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
a.setDirection(123);
|
||||
t.equals(a._getRenderedDirectionAndScale().direction, 123);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setVisible', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
a.setVisible(true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setSize', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
a.setSize(123);
|
||||
t.equals(a._getRenderedDirectionAndScale().scale[0], 123);
|
||||
renderer.getCurrentSkinSize = () => [100, 100];
|
||||
a.setSize(99999);
|
||||
t.equals(a._getRenderedDirectionAndScale().scale[0], 540);
|
||||
r.setRuntimeOptions({
|
||||
fencing: false
|
||||
});
|
||||
a.setSize(99999);
|
||||
t.equals(a._getRenderedDirectionAndScale().scale[0], 99999);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('set and clear effects', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
for (const effect in a.effects) {
|
||||
a.setEffect(effect, 1);
|
||||
t.equals(a.effects[effect], 1);
|
||||
}
|
||||
a.clearEffects();
|
||||
for (const effect in a.effects) {
|
||||
t.equals(a.effects[effect], 0);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setCostume', t => {
|
||||
const o = new Object();
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
s.costumes = [o];
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
a.setCostume(0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteCostume', t => {
|
||||
const o1 = {id: 1};
|
||||
const o2 = {id: 2};
|
||||
const o3 = {id: 3};
|
||||
const o4 = {id: 4};
|
||||
const o5 = {id: 5};
|
||||
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
s.costumes = [o1, o2, o3];
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
|
||||
// x* Costume 1 * Costume 2
|
||||
// Costume 2 => Costume 3
|
||||
// Costume 3
|
||||
a.setCostume(0);
|
||||
const deletedCostume = a.deleteCostume(0);
|
||||
t.equals(a.sprite.costumes.length, 2);
|
||||
t.equals(a.sprite.costumes[0].id, 2);
|
||||
t.equals(a.sprite.costumes[1].id, 3);
|
||||
t.equals(a.currentCostume, 0);
|
||||
t.deepEqual(deletedCostume, o1);
|
||||
|
||||
// Costume 1 Costume 1
|
||||
// x* Costume 2 => * Costume 3
|
||||
// Costume 3
|
||||
a.sprite.costumes = [o1, o2, o3];
|
||||
a.setCostume(1);
|
||||
const deletedCostume2 = a.deleteCostume(1);
|
||||
t.equals(a.sprite.costumes.length, 2);
|
||||
t.equals(a.sprite.costumes[0].id, 1);
|
||||
t.equals(a.sprite.costumes[1].id, 3);
|
||||
t.equals(a.currentCostume, 1);
|
||||
t.deepEqual(deletedCostume2, o2);
|
||||
|
||||
// Costume 1 Costume 1
|
||||
// Costume 2 => * Costume 2
|
||||
// x* Costume 3
|
||||
a.sprite.costumes = [o1, o2, o3];
|
||||
a.setCostume(2);
|
||||
const deletedCostume3 = a.deleteCostume(2);
|
||||
t.equals(a.sprite.costumes.length, 2);
|
||||
t.equals(a.sprite.costumes[0].id, 1);
|
||||
t.equals(a.sprite.costumes[1].id, 2);
|
||||
t.equals(a.currentCostume, 1);
|
||||
t.deepEqual(deletedCostume3, o3);
|
||||
|
||||
// Refuses to delete only costume
|
||||
a.sprite.costumes = [o1];
|
||||
a.setCostume(0);
|
||||
const noDeletedCostume = a.deleteCostume(0);
|
||||
t.equals(a.sprite.costumes.length, 1);
|
||||
t.equals(a.sprite.costumes[0].id, 1);
|
||||
t.equals(a.currentCostume, 0);
|
||||
t.equal(noDeletedCostume, null);
|
||||
|
||||
// Costume 1 Costume 1
|
||||
// x Costume 2 Costume 3
|
||||
// Costume 3 => * Costume 4
|
||||
// * Costume 4 Costume 5
|
||||
// Costume 5
|
||||
a.sprite.costumes = [o1, o2, o3, o4, o5];
|
||||
a.setCostume(3);
|
||||
a.deleteCostume(1);
|
||||
t.equals(a.sprite.costumes.length, 4);
|
||||
t.equals(a.sprite.costumes[0].id, 1);
|
||||
t.equals(a.sprite.costumes[1].id, 3);
|
||||
t.equals(a.sprite.costumes[2].id, 4);
|
||||
t.equals(a.sprite.costumes[3].id, 5);
|
||||
t.equals(a.currentCostume, 2);
|
||||
|
||||
// Costume 1 Costume 1
|
||||
// * Costume 2 * Costume 2
|
||||
// Costume 3 => Costume 3
|
||||
// x Costume 4 Costume 5
|
||||
// Costume 5
|
||||
a.sprite.costumes = [o1, o2, o3, o4, o5];
|
||||
a.setCostume(1);
|
||||
a.deleteCostume(3);
|
||||
t.equals(a.sprite.costumes.length, 4);
|
||||
t.equals(a.sprite.costumes[0].id, 1);
|
||||
t.equals(a.sprite.costumes[1].id, 2);
|
||||
t.equals(a.sprite.costumes[2].id, 3);
|
||||
t.equals(a.sprite.costumes[3].id, 5);
|
||||
t.equals(a.currentCostume, 1);
|
||||
|
||||
// Costume 1 Costume 1
|
||||
// * Costume 2 * Costume 2
|
||||
// Costume 3 => Costume 3
|
||||
// Costume 4 Costume 4
|
||||
// x Costume 5
|
||||
a.sprite.costumes = [o1, o2, o3, o4, o5];
|
||||
a.setCostume(1);
|
||||
a.deleteCostume(4);
|
||||
t.equals(a.sprite.costumes.length, 4);
|
||||
t.equals(a.sprite.costumes[0].id, 1);
|
||||
t.equals(a.sprite.costumes[1].id, 2);
|
||||
t.equals(a.sprite.costumes[2].id, 3);
|
||||
t.equals(a.sprite.costumes[3].id, 4);
|
||||
t.equals(a.currentCostume, 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteSound', t => {
|
||||
const o1 = {id: 1};
|
||||
const o2 = {id: 2};
|
||||
const o3 = {id: 3};
|
||||
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
s.sounds = [o1, o2, o3];
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
|
||||
const firstDeleted = a.deleteSound(0);
|
||||
t.deepEqual(a.sprite.sounds, [o2, o3]);
|
||||
t.deepEqual(firstDeleted, o1);
|
||||
|
||||
// Allows deleting the only sound
|
||||
a.sprite.sounds = [o1];
|
||||
a.deleteSound(0);
|
||||
t.deepEqual(a.sprite.sounds, []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setRotationStyle', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
a.setRotationStyle(RenderedTarget.ROTATION_STYLE_NONE);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getBounds', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.getBounds().top, 0);
|
||||
a.setXY(241, 241);
|
||||
t.equals(a.getBounds().top, 241);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isTouchingPoint', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.isTouchingPoint(), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isTouchingEdge', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.isTouchingEdge(), false);
|
||||
a.setXY(1000, 1000);
|
||||
t.equals(a.isTouchingEdge(), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isTouchingSprite', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.isTouchingSprite('fake'), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isTouchingColor', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.isTouchingColor(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('colorIsTouchingColor', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.colorIsTouchingColor(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('layers', t => { // TODO this tests fake functionality. Move layering tests into Render.
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
const o = new Object();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
a.goToFront();
|
||||
t.equals(a.renderer.order, 5);
|
||||
a.goBackwardLayers(2);
|
||||
t.equals(a.renderer.order, 3);
|
||||
a.goToBack();
|
||||
// Note, there are only sprites in this test, no stage, and the addition
|
||||
// of layer groups, goToBack no longer specifies a minimum order number
|
||||
t.equals(a.renderer.order, 0);
|
||||
a.goForwardLayers(1);
|
||||
t.equals(a.renderer.order, 1);
|
||||
o.drawableID = 999;
|
||||
a.goBehindOther(o);
|
||||
t.equals(a.renderer.order, 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getLayerOrder returns result of renderer getDrawableOrder or null if renderer is not attached', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
|
||||
// getLayerOrder should return null if there is no renderer attached to the runtime
|
||||
t.equal(a.getLayerOrder(), null);
|
||||
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const b = new RenderedTarget(s, r);
|
||||
|
||||
t.equal(b.getLayerOrder(), 'stub');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('keepInFence', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const a = new RenderedTarget(s, r);
|
||||
a.renderer = renderer;
|
||||
t.equals(a.keepInFence(1000, 1000)[0], 240);
|
||||
t.equals(a.keepInFence(-1000, 1000)[0], -240);
|
||||
t.equals(a.keepInFence(1000, 1000)[1], 180);
|
||||
t.equals(a.keepInFence(1000, -1000)[1], -180);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#stopAll clears graphics effects', t => {
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
const a = new RenderedTarget(s, r);
|
||||
const effectName = 'brightness';
|
||||
a.setEffect(effectName, 100);
|
||||
a.onStopAll();
|
||||
t.equals(a.effects[effectName], 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#getCostumes returns the costumes', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
a.sprite.costumes = [{id: 1}, {id: 2}, {id: 3}];
|
||||
t.equals(a.getCostumes().length, 3);
|
||||
t.equals(a.getCostumes()[0].id, 1);
|
||||
t.equals(a.getCostumes()[1].id, 2);
|
||||
t.equals(a.getCostumes()[2].id, 3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#getSounds returns the sounds', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
const sounds = [1, 2, 3];
|
||||
a.sprite.sounds = sounds;
|
||||
t.equals(a.getSounds(), sounds);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#toJSON returns the sounds and costumes', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
const sounds = [1, 2, 3];
|
||||
a.sprite.sounds = sounds;
|
||||
a.sprite.costumes = [{id: 1}, {id: 2}, {id: 3}];
|
||||
t.same(a.toJSON().sounds, sounds);
|
||||
t.same(a.toJSON().costumes, a.sprite.costumes);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#addSound does not duplicate names', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
a.sprite.sounds = [{name: 'first'}];
|
||||
a.addSound({name: 'first'});
|
||||
t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#addCostume does not duplicate names', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
a.addCostume({name: 'first'});
|
||||
a.addCostume({name: 'first'});
|
||||
t.equal(a.sprite.costumes.length, 2);
|
||||
t.equal(a.sprite.costumes[0].name, 'first');
|
||||
t.equal(a.sprite.costumes[1].name, 'first2');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#renameSound does not duplicate names', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
a.sprite.sounds = [{name: 'first'}, {name: 'second'}];
|
||||
a.renameSound(0, 'first'); // Shouldn't increment the name, noop
|
||||
t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'second'}]);
|
||||
a.renameSound(1, 'first');
|
||||
t.deepEqual(a.sprite.sounds, [{name: 'first'}, {name: 'first2'}]);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#renameCostume does not duplicate names', t => {
|
||||
const r = new Runtime();
|
||||
const spr = new Sprite(null, r);
|
||||
const a = new RenderedTarget(spr, r);
|
||||
a.sprite.costumes = [{name: 'first'}, {name: 'second'}];
|
||||
a.renameCostume(0, 'first'); // Shouldn't increment the name, noop
|
||||
t.equal(a.sprite.costumes.length, 2);
|
||||
t.equal(a.sprite.costumes[0].name, 'first');
|
||||
t.equal(a.sprite.costumes[1].name, 'second');
|
||||
a.renameCostume(1, 'first');
|
||||
t.equal(a.sprite.costumes.length, 2);
|
||||
t.equal(a.sprite.costumes[0].name, 'first');
|
||||
t.equal(a.sprite.costumes[1].name, 'first2');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#reorderCostume', t => {
|
||||
const o1 = {id: 0};
|
||||
const o2 = {id: 1};
|
||||
const o3 = {id: 2};
|
||||
const o4 = {id: 3};
|
||||
const o5 = {id: 4};
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
s.costumes = [o1, o2, o3, o4, o5];
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
|
||||
const resetCostumes = () => {
|
||||
a.setCostume(0);
|
||||
s.costumes = [o1, o2, o3, o4, o5];
|
||||
};
|
||||
const costumeIds = () => a.sprite.costumes.map(c => c.id);
|
||||
|
||||
resetCostumes();
|
||||
t.deepEquals(costumeIds(), [0, 1, 2, 3, 4]);
|
||||
t.equals(a.currentCostume, 0);
|
||||
|
||||
// Returns false if the costumes are the same and no change occurred
|
||||
t.equal(a.reorderCostume(3, 3), false);
|
||||
t.equal(a.reorderCostume(999, 5000), false); // Clamped to the same values.
|
||||
t.equal(a.reorderCostume(-999, -5000), false);
|
||||
|
||||
// Make sure reordering up and down works and current costume follows
|
||||
resetCostumes();
|
||||
t.equal(a.reorderCostume(0, 3), true);
|
||||
t.deepEquals(costumeIds(), [1, 2, 3, 0, 4]);
|
||||
t.equals(a.currentCostume, 3); // Index of id=0
|
||||
|
||||
resetCostumes();
|
||||
a.setCostume(1);
|
||||
t.equal(a.reorderCostume(3, 1), true);
|
||||
t.deepEquals(costumeIds(), [0, 3, 1, 2, 4]);
|
||||
t.equals(a.currentCostume, 2); // Index of id=1
|
||||
|
||||
// Out of bounds indices get clamped
|
||||
resetCostumes();
|
||||
t.equal(a.reorderCostume(10, 0), true);
|
||||
t.deepEquals(costumeIds(), [4, 0, 1, 2, 3]);
|
||||
t.equals(a.currentCostume, 1); // Index of id=0
|
||||
|
||||
resetCostumes();
|
||||
t.equal(a.reorderCostume(2, -1000), true);
|
||||
t.deepEquals(costumeIds(), [2, 0, 1, 3, 4]);
|
||||
t.equals(a.currentCostume, 1); // Index of id=0
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('#reorderSound', t => {
|
||||
const o1 = {id: 0, name: 'name0'};
|
||||
const o2 = {id: 1, name: 'name1'};
|
||||
const o3 = {id: 2, name: 'name2'};
|
||||
const o4 = {id: 3, name: 'name3'};
|
||||
const o5 = {id: 4, name: 'name4'};
|
||||
const r = new Runtime();
|
||||
const s = new Sprite(null, r);
|
||||
s.sounds = [o1, o2, o3, o4, o5];
|
||||
const a = new RenderedTarget(s, r);
|
||||
const renderer = new FakeRenderer();
|
||||
a.renderer = renderer;
|
||||
|
||||
const resetSounds = () => {
|
||||
s.sounds = [o1, o2, o3, o4, o5];
|
||||
};
|
||||
const soundIds = () => a.sprite.sounds.map(c => c.id);
|
||||
|
||||
resetSounds();
|
||||
t.deepEquals(soundIds(), [0, 1, 2, 3, 4]);
|
||||
|
||||
// Return false if indices are the same and no change occurred.
|
||||
t.equal(a.reorderSound(3, 3), false);
|
||||
t.equal(a.reorderSound(100000, 99999), false); // Clamped to the same values
|
||||
t.equal(a.reorderSound(-100000, -99999), false);
|
||||
|
||||
// Make sure reordering up and down works and current sound follows
|
||||
resetSounds();
|
||||
t.equal(a.reorderSound(0, 3), true);
|
||||
t.deepEquals(soundIds(), [1, 2, 3, 0, 4]);
|
||||
|
||||
resetSounds();
|
||||
t.equal(a.reorderSound(3, 1), true);
|
||||
t.deepEquals(soundIds(), [0, 3, 1, 2, 4]);
|
||||
|
||||
// Out of bounds indices get clamped
|
||||
resetSounds();
|
||||
t.equal(a.reorderSound(10, 0), true);
|
||||
t.deepEquals(soundIds(), [4, 0, 1, 2, 3]);
|
||||
|
||||
resetSounds();
|
||||
t.equal(a.reorderSound(2, -1000), true);
|
||||
t.deepEquals(soundIds(), [2, 0, 1, 3, 4]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
57
scratch-vm/test/unit/tw_asset_util.js
Normal file
57
scratch-vm/test/unit/tw_asset_util.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const {test} = require('tap');
|
||||
const JSZip = require('@turbowarp/jszip');
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const AssetUtil = require('../../src/util/tw-asset-util');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('getByMd5ext from zip root', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
rt.storage.load = () => t.fail('should not call storage.load()');
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('00000000000000000000000000000000.svg', new Uint8Array([1, 2, 3]));
|
||||
|
||||
AssetUtil.getByMd5ext(rt, zip, rt.storage.AssetType.SVG, '00000000000000000000000000000000.svg')
|
||||
.then(asset => {
|
||||
t.same(asset.data, new Uint8Array([1, 2, 3]));
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('getByMd5ext from zip subdirectory', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
rt.storage.load = () => t.fail('should not call storage.load()');
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('folder/00000000000000000000000000000000.svg', new Uint8Array([25, 26, 27]));
|
||||
|
||||
AssetUtil.getByMd5ext(rt, zip, rt.storage.AssetType.SVG, '00000000000000000000000000000000.svg')
|
||||
.then(asset => {
|
||||
t.same(asset.data, new Uint8Array([25, 26, 27]));
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('getByMd5ext from storage with null zip', t => {
|
||||
t.plan(4);
|
||||
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
|
||||
rt.storage.load = (assetType, md5, ext) => {
|
||||
t.equal(assetType, rt.storage.AssetType.SVG);
|
||||
t.equal(md5, '00000000000000000000000000000000');
|
||||
t.equal(ext, 'svg');
|
||||
return Promise.resolve({
|
||||
fromStorage: true
|
||||
});
|
||||
};
|
||||
|
||||
AssetUtil.getByMd5ext(rt, null, rt.storage.AssetType.SVG, '00000000000000000000000000000000.svg')
|
||||
.then(asset => {
|
||||
t.ok(asset.fromStorage);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
130
scratch-vm/test/unit/tw_block_colors.js
Normal file
130
scratch-vm/test/unit/tw_block_colors.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('with explicit category colors', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
color1: '#ff0000',
|
||||
color2: '#00ff00',
|
||||
color3: '#0000ff',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'defaults'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'color1',
|
||||
color1: '#ffff00'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'color2',
|
||||
color2: '#ff00ff'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'color3',
|
||||
color3: '#00ffff'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'colorAll',
|
||||
color1: '#7fff00',
|
||||
color2: '#ff007f',
|
||||
color3: '#007fff'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
|
||||
t.equal(blocks[0].colour, '#ff0000');
|
||||
t.equal(blocks[0].colourSecondary, '#00ff00');
|
||||
t.equal(blocks[0].colourTertiary, '#0000ff');
|
||||
|
||||
t.equal(blocks[1].colour, '#ffff00');
|
||||
t.equal(blocks[1].colourSecondary, '#00ff00');
|
||||
t.equal(blocks[1].colourTertiary, '#0000ff');
|
||||
|
||||
t.equal(blocks[2].colour, '#ff0000');
|
||||
t.equal(blocks[2].colourSecondary, '#ff00ff');
|
||||
t.equal(blocks[2].colourTertiary, '#0000ff');
|
||||
|
||||
t.equal(blocks[3].colour, '#ff0000');
|
||||
t.equal(blocks[3].colourSecondary, '#00ff00');
|
||||
t.equal(blocks[3].colourTertiary, '#00ffff');
|
||||
|
||||
t.equal(blocks[4].colour, '#7fff00');
|
||||
t.equal(blocks[4].colourSecondary, '#ff007f');
|
||||
t.equal(blocks[4].colourTertiary, '#007fff');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('with the default colors', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'defaults'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'color1',
|
||||
color1: '#ffff00'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'color2',
|
||||
color2: '#ff00ff'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'color3',
|
||||
color3: '#00ffff'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'colorAll',
|
||||
color1: '#7fff00',
|
||||
color2: '#ff007f',
|
||||
color3: '#007fff'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
|
||||
t.equal(blocks[0].colour, '#0FBD8C');
|
||||
t.equal(blocks[0].colourSecondary, '#0DA57A');
|
||||
t.equal(blocks[0].colourTertiary, '#0B8E69');
|
||||
|
||||
t.equal(blocks[1].colour, '#ffff00');
|
||||
t.equal(blocks[1].colourSecondary, '#0DA57A');
|
||||
t.equal(blocks[1].colourTertiary, '#0B8E69');
|
||||
|
||||
t.equal(blocks[2].colour, '#0FBD8C');
|
||||
t.equal(blocks[2].colourSecondary, '#ff00ff');
|
||||
t.equal(blocks[2].colourTertiary, '#0B8E69');
|
||||
|
||||
t.equal(blocks[3].colour, '#0FBD8C');
|
||||
t.equal(blocks[3].colourSecondary, '#0DA57A');
|
||||
t.equal(blocks[3].colourTertiary, '#00ffff');
|
||||
|
||||
t.equal(blocks[4].colour, '#7fff00');
|
||||
t.equal(blocks[4].colourSecondary, '#ff007f');
|
||||
t.equal(blocks[4].colourTertiary, '#007fff');
|
||||
|
||||
t.end();
|
||||
});
|
||||
152
scratch-vm/test/unit/tw_block_extensions.js
Normal file
152
scratch-vm/test/unit/tw_block_extensions.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('does not duplicate', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blockIconURI: 'data:whatever',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'test',
|
||||
extensions: [
|
||||
'something_invalid',
|
||||
'from_extension',
|
||||
'scratch_extension',
|
||||
'default_extension_colors'
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
t.same(
|
||||
blocks[0].extensions,
|
||||
['from_extension', 'default_extension_colors', 'scratch_extension', 'something_invalid']
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('block icon', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'nothing'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'empty',
|
||||
extensions: []
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'iconNothing',
|
||||
blockIconURI: 'data:whatever'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'iconEmpty',
|
||||
blockIconURI: 'data:whatever',
|
||||
extensions: []
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'iconSomething',
|
||||
blockIconURI: 'data:whatever',
|
||||
extensions: ['colours_sensing']
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
t.same(blocks[0].extensions, ['from_extension', 'default_extension_colors']);
|
||||
t.same(blocks[1].extensions, ['from_extension', 'default_extension_colors']);
|
||||
t.same(blocks[2].extensions, ['from_extension', 'default_extension_colors', 'scratch_extension']);
|
||||
t.same(blocks[3].extensions, ['from_extension', 'default_extension_colors', 'scratch_extension']);
|
||||
t.same(
|
||||
blocks[4].extensions,
|
||||
['from_extension', 'default_extension_colors', 'scratch_extension', 'colours_sensing']
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('category icon', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blockIconURI: 'data:whatever',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'nothing'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
t.same(blocks[0].extensions, ['from_extension', 'default_extension_colors', 'scratch_extension']);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('category color', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blockIconURI: 'data:whatever',
|
||||
color1: '#123456',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'nothing'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
t.same(blocks[0].extensions, ['from_extension', 'scratch_extension']);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('category color', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'default'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'custom',
|
||||
color3: '#123456'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
t.same(blocks[0].extensions, ['from_extension', 'default_extension_colors']);
|
||||
t.same(blocks[1].extensions, ['from_extension']);
|
||||
t.end();
|
||||
});
|
||||
46
scratch-vm/test/unit/tw_branch_icon_uri.js
Normal file
46
scratch-vm/test/unit/tw_branch_icon_uri.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const {test} = require('tap');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
|
||||
test('branchIconURI', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.extensionManager._registerInternalExtension({
|
||||
getInfo: () => ({
|
||||
id: 'testextension',
|
||||
name: 'test',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.LOOP,
|
||||
opcode: 'block1',
|
||||
text: 'no custom icon'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.LOOP,
|
||||
opcode: 'block2',
|
||||
text: 'LOOP with custom icon',
|
||||
branchIconURI: 'data:whatever1'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.CONDITIONAL,
|
||||
opcode: 'block3',
|
||||
text: 'CONDITIONAL with custom icon',
|
||||
branchIconURI: 'data:whatever2'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.LOOP,
|
||||
opcode: 'block4',
|
||||
text: 'LOOP with no icon',
|
||||
branchIconURI: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const blocks = vm.runtime.getBlocksJSON();
|
||||
t.equal(blocks[0].args2[0].src, 'media://repeat.svg', 'default custom icon');
|
||||
t.equal(blocks[1].args2[0].src, 'data:whatever1', 'LOOP with custom icon');
|
||||
t.equal(blocks[2].args2[0].src, 'data:whatever2', 'CONDITIONAL with custom icon');
|
||||
t.same(blocks[3].args2, null, 'LOOP with no icon');
|
||||
|
||||
t.end();
|
||||
});
|
||||
83
scratch-vm/test/unit/tw_cast.js
Normal file
83
scratch-vm/test/unit/tw_cast.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const Cast = require('../../src/util/cast');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('Cast.compare with assorted whitespace characters', t => {
|
||||
t.equal(Cast.compare('', ''), 0);
|
||||
|
||||
t.equal(Cast.compare(' ', ''), 1);
|
||||
t.equal(Cast.compare('', ' '), -1);
|
||||
|
||||
t.equal(Cast.compare(' ', ' '), -1);
|
||||
t.equal(Cast.compare(' ', ' '), 1);
|
||||
|
||||
t.equal(Cast.compare(' \u00a0 ', '\r\n'), 1);
|
||||
t.equal(Cast.compare('\r\n', ' \u00a0 '), -1);
|
||||
|
||||
t.equal(Cast.compare(' 0', 0), 0);
|
||||
t.equal(Cast.compare(0, ' 0'), 0);
|
||||
|
||||
t.equal(Cast.compare(' 0 ', ' \r\n\u00a0 0 \n\n\n\n'), 0);
|
||||
t.equal(Cast.compare(' \r\n\u00a0 0 \n\n\n\n', ' 0 '), 0);
|
||||
|
||||
t.equal(Cast.compare(' 0 ', ' \r\n\u00a0 0 \n\n\n\b'), 1);
|
||||
t.equal(Cast.compare(' \r\n\u00a0 0 \n\n\n\b', ' 0 '), -1);
|
||||
|
||||
t.equal(Cast.compare(' 0', '0'), 0);
|
||||
t.equal(Cast.compare('0', ' 0'), 0);
|
||||
|
||||
t.equal(Cast.compare('', 0), -1);
|
||||
t.equal(Cast.compare(0, ''), 1);
|
||||
|
||||
t.equal(Cast.compare(' ', 0), -1);
|
||||
t.equal(Cast.compare(0, ' '), 1);
|
||||
|
||||
t.equal(Cast.compare('0', ' '), 1);
|
||||
t.equal(Cast.compare(' ', '0'), -1);
|
||||
|
||||
t.equal(Cast.compare('\n0', '\n-1'), 1);
|
||||
t.equal(Cast.compare('\n-1', '\n0'), -1);
|
||||
|
||||
t.equal(Cast.compare('', 'false'), -1);
|
||||
t.equal(Cast.compare('false', ''), 1);
|
||||
|
||||
t.equal(Cast.compare('', ' false'), -1);
|
||||
t.equal(Cast.compare(' false', ''), 1);
|
||||
|
||||
t.equal(Cast.compare('\n', ' false'), -1);
|
||||
t.equal(Cast.compare('false', '\n'), 1);
|
||||
|
||||
t.equal(Cast.compare(false, ''), 1);
|
||||
t.equal(Cast.compare('', false), -1);
|
||||
|
||||
t.equal(Cast.compare(false, ' '), 1);
|
||||
t.equal(Cast.compare(' ', false), -1);
|
||||
|
||||
t.equal(Cast.compare('\t', '0'), 0);
|
||||
t.equal(Cast.compare('0', '\t'), 0);
|
||||
|
||||
t.equal(Cast.compare('\t', 0), 0);
|
||||
t.equal(Cast.compare(0, '\t'), 0);
|
||||
|
||||
t.equal(Cast.compare('\t', ''), 1);
|
||||
t.equal(Cast.compare('', '\t'), -1);
|
||||
|
||||
t.equal(Cast.compare(' \t ', '0'), 0);
|
||||
t.equal(Cast.compare('0', ' \t '), 0);
|
||||
|
||||
t.equal(Cast.compare('\r\n \t\u00a0', 0), 0);
|
||||
t.equal(Cast.compare(0, '\r\n \t\u00a0'), 0);
|
||||
|
||||
t.equal(Cast.compare('\t', false), 0);
|
||||
t.equal(Cast.compare(false, '\t'), 0);
|
||||
|
||||
t.equal(Cast.compare('\t', 'false'), -1);
|
||||
t.equal(Cast.compare('false', '\t'), 1);
|
||||
|
||||
t.equal(Cast.compare('\t', '1'), -1);
|
||||
t.equal(Cast.compare('1', '\t'), 1);
|
||||
|
||||
t.equal(Cast.compare('\t', 1), -1);
|
||||
t.equal(Cast.compare(1, '\t'), 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
18
scratch-vm/test/unit/tw_clones.js
Normal file
18
scratch-vm/test/unit/tw_clones.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
|
||||
const {test} = require('tap');
|
||||
|
||||
test('clone counter', t => {
|
||||
const rt = new Runtime();
|
||||
const sprite = new Sprite(null, rt);
|
||||
const original = sprite.createClone();
|
||||
t.equal(rt._cloneCounter, 0);
|
||||
const clone = original.makeClone();
|
||||
t.equal(rt._cloneCounter, 1);
|
||||
clone.dispose();
|
||||
t.equal(rt._cloneCounter, 0);
|
||||
original.dispose();
|
||||
t.equal(rt._cloneCounter, 0);
|
||||
t.end();
|
||||
});
|
||||
318
scratch-vm/test/unit/tw_compress.js
Normal file
318
scratch-vm/test/unit/tw_compress.js
Normal file
@@ -0,0 +1,318 @@
|
||||
const {test} = require('tap');
|
||||
const compress = require('../../src/serialization/tw-compress-sb3');
|
||||
const uid = require('../../src/util/uid');
|
||||
|
||||
test('handles type INPUT_DIFF_BLOCK_SHADOW (3) compressed inputs', t => {
|
||||
const data = {
|
||||
targets: [
|
||||
{
|
||||
isStage: true,
|
||||
name: 'Stage',
|
||||
variables: {},
|
||||
lists: {},
|
||||
broadcasts: {},
|
||||
blocks: {
|
||||
'CmRa^i]o}QL77;hk:54o': {
|
||||
opcode: 'looks_switchbackdropto',
|
||||
next: null,
|
||||
parent: null,
|
||||
inputs: {
|
||||
BACKDROP: [
|
||||
3,
|
||||
'cq84G6uywD{m2R,E03Ci',
|
||||
'E3/*4H*xk38{=*U;bVWm'
|
||||
]
|
||||
},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 409,
|
||||
y: 300
|
||||
},
|
||||
'cq84G6uywD{m2R,E03Ci': {
|
||||
opcode: 'operator_not',
|
||||
next: null,
|
||||
parent: 'CmRa^i]o}QL77;hk:54o',
|
||||
inputs: {},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: false
|
||||
},
|
||||
'E3/*4H*xk38{=*U;bVWm': {
|
||||
opcode: 'looks_backdrops',
|
||||
next: null,
|
||||
parent: 'CmRa^i]o}QL77;hk:54o',
|
||||
inputs: {},
|
||||
fields: {
|
||||
BACKDROP: [
|
||||
'backdrop1',
|
||||
null
|
||||
]
|
||||
},
|
||||
shadow: true,
|
||||
topLevel: false
|
||||
}
|
||||
},
|
||||
comments: {},
|
||||
currentCostume: 0,
|
||||
costumes: [],
|
||||
sounds: [],
|
||||
volume: 100,
|
||||
layerOrder: 0,
|
||||
tempo: 60,
|
||||
videoTransparency: 50,
|
||||
videoState: 'on',
|
||||
textToSpeechLanguage: null
|
||||
}
|
||||
],
|
||||
monitors: [],
|
||||
extensions: [],
|
||||
meta: {
|
||||
semver: '3.0.0',
|
||||
vm: '0.2.0',
|
||||
agent: ''
|
||||
}
|
||||
};
|
||||
compress(data);
|
||||
|
||||
const blocks = Object.entries(data.targets[0].blocks);
|
||||
t.equal(blocks.length, 3);
|
||||
|
||||
const [parentId, parentBlock] = blocks.find(i => i[1].opcode === 'looks_switchbackdropto');
|
||||
const [inputId, inputBlock] = blocks.find(i => i[1].opcode === 'operator_not');
|
||||
const [shadowId, shadowBlock] = blocks.find(i => i[1].opcode === 'looks_backdrops');
|
||||
|
||||
t.equal(parentBlock.inputs.BACKDROP.length, 3);
|
||||
t.equal(parentBlock.inputs.BACKDROP[0], 3);
|
||||
t.equal(parentBlock.inputs.BACKDROP[1], inputId);
|
||||
t.equal(parentBlock.inputs.BACKDROP[2], shadowId);
|
||||
|
||||
t.equal(inputBlock.parent, parentId);
|
||||
t.equal(shadowBlock.parent, parentId);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Compressed IDs will not collide with uncompressed IDs', t => {
|
||||
const soup = 'abcdefghjijklmnopqstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
|
||||
const items = [
|
||||
[soup, '', ''],
|
||||
['', soup, ''],
|
||||
['', '', soup]
|
||||
];
|
||||
for (const [variableSoup, listSoup, broadcastSoup] of items) {
|
||||
const data = {
|
||||
targets: [
|
||||
{
|
||||
isStage: true,
|
||||
name: 'Stage',
|
||||
variables: Object.fromEntries(
|
||||
variableSoup.split('').map(id => [id, [id, 0]])
|
||||
),
|
||||
lists: Object.fromEntries(
|
||||
listSoup.split('').map(id => [id, [id, []]])
|
||||
),
|
||||
broadcasts: Object.fromEntries(
|
||||
broadcastSoup.split('').map(id => [id, id])
|
||||
),
|
||||
blocks: {
|
||||
'CmRa^i]o}QL77;hk:54o': {
|
||||
opcode: 'looks_switchbackdropto',
|
||||
next: null,
|
||||
parent: null,
|
||||
inputs: {
|
||||
BACKDROP: [
|
||||
3,
|
||||
'cq84G6uywD{m2R,E03Ci',
|
||||
'E3/*4H*xk38{=*U;bVWm'
|
||||
]
|
||||
},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 409,
|
||||
y: 300
|
||||
},
|
||||
'cq84G6uywD{m2R,E03Ci': {
|
||||
opcode: 'operator_not',
|
||||
next: null,
|
||||
parent: 'CmRa^i]o}QL77;hk:54o',
|
||||
inputs: {},
|
||||
fields: {},
|
||||
shadow: false,
|
||||
topLevel: false
|
||||
},
|
||||
'E3/*4H*xk38{=*U;bVWm': {
|
||||
opcode: 'looks_backdrops',
|
||||
next: null,
|
||||
parent: 'CmRa^i]o}QL77;hk:54o',
|
||||
inputs: {},
|
||||
fields: {
|
||||
BACKDROP: [
|
||||
'backdrop1',
|
||||
null
|
||||
]
|
||||
},
|
||||
shadow: true,
|
||||
topLevel: false
|
||||
}
|
||||
},
|
||||
comments: {
|
||||
'ds{.EoY%0^6vO1WH0/9d': {
|
||||
blockId: null,
|
||||
x: 400,
|
||||
y: 401,
|
||||
width: 402,
|
||||
height: 403,
|
||||
minimized: false,
|
||||
text: '4'
|
||||
},
|
||||
'blh[bsi@XtCkGh!-J5aa': {
|
||||
blockId: null,
|
||||
x: 500,
|
||||
y: 501,
|
||||
width: 502,
|
||||
height: 503,
|
||||
minimized: false,
|
||||
text: '5'
|
||||
},
|
||||
'7#YgytOiJHs(Ne6,2i9(': {
|
||||
blockId: null,
|
||||
x: 600,
|
||||
y: 601,
|
||||
width: 602,
|
||||
height: 603,
|
||||
minimized: false,
|
||||
text: '6'
|
||||
}
|
||||
},
|
||||
currentCostume: 0,
|
||||
costumes: [],
|
||||
sounds: [],
|
||||
volume: 100,
|
||||
layerOrder: 0,
|
||||
tempo: 60,
|
||||
videoTransparency: 50,
|
||||
videoState: 'on',
|
||||
textToSpeechLanguage: null
|
||||
}
|
||||
],
|
||||
monitors: [],
|
||||
extensions: [],
|
||||
meta: {
|
||||
semver: '3.0.0',
|
||||
vm: '0.2.0',
|
||||
agent: ''
|
||||
}
|
||||
};
|
||||
compress(data);
|
||||
|
||||
const uncompressedIDs = [
|
||||
...Object.keys(data.targets[0].variables),
|
||||
...Object.keys(data.targets[0].lists),
|
||||
...Object.keys(data.targets[0].broadcasts)
|
||||
];
|
||||
const compressedIDs = [
|
||||
...Object.keys(data.targets[0].blocks),
|
||||
...Object.keys(data.targets[0].comments)
|
||||
];
|
||||
for (const compressedID of compressedIDs) {
|
||||
t.notOk(uncompressedIDs.includes(compressedID), `${compressedID} does not collide`);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Script execution order is preserved', t => {
|
||||
const originalBlocks = {};
|
||||
|
||||
const blockIds = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
if (i === 339) {
|
||||
blockIds.push('muffin');
|
||||
} else if (i === 555) {
|
||||
blockIds.push('555');
|
||||
}
|
||||
blockIds.push(uid());
|
||||
}
|
||||
blockIds.push('apple');
|
||||
blockIds.push('-1');
|
||||
blockIds.push('45');
|
||||
|
||||
for (const blockId of blockIds) {
|
||||
originalBlocks[blockId] = {
|
||||
opcode: 'event_whenbroadcastreceived',
|
||||
next: null,
|
||||
parent: null,
|
||||
inputs: {},
|
||||
fields: {
|
||||
BROADCAST_OPTION: [
|
||||
`broadcast-name-${blockId}`,
|
||||
`broadcast-id-${blockId}`
|
||||
]
|
||||
},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: -10,
|
||||
y: 420
|
||||
};
|
||||
}
|
||||
|
||||
const data = {
|
||||
targets: [
|
||||
{
|
||||
isStage: true,
|
||||
name: 'Stage',
|
||||
variables: {},
|
||||
lists: {},
|
||||
broadcasts: {},
|
||||
blocks: originalBlocks,
|
||||
comments: {},
|
||||
currentCostume: 0,
|
||||
costumes: [],
|
||||
sounds: [],
|
||||
volume: 100,
|
||||
layerOrder: 0,
|
||||
tempo: 60,
|
||||
videoTransparency: 50,
|
||||
videoState: 'on',
|
||||
textToSpeechLanguage: null
|
||||
}
|
||||
],
|
||||
monitors: [],
|
||||
extensions: [],
|
||||
meta: {
|
||||
semver: '3.0.0',
|
||||
vm: '0.2.0',
|
||||
agent: ''
|
||||
}
|
||||
};
|
||||
compress(data);
|
||||
|
||||
// Sanity check: Make sure the new object is actually different
|
||||
const newBlocks = data.targets[0].blocks;
|
||||
t.not(originalBlocks, newBlocks);
|
||||
t.notSame(Object.keys(originalBlocks), Object.keys(newBlocks));
|
||||
|
||||
// Check that the order has not changed
|
||||
const newBlockValues = Object.values(newBlocks);
|
||||
t.same(Object.values(originalBlocks), newBlockValues);
|
||||
t.equal(newBlockValues[0].fields.BROADCAST_OPTION[0], 'broadcast-name-45');
|
||||
t.equal(newBlockValues[1].fields.BROADCAST_OPTION[0], 'broadcast-name-555');
|
||||
t.equal(newBlockValues[339 + 2].fields.BROADCAST_OPTION[0], 'broadcast-name-muffin');
|
||||
t.equal(newBlockValues[newBlockValues.length - 2].fields.BROADCAST_OPTION[0], 'broadcast-name-apple');
|
||||
t.equal(newBlockValues[newBlockValues.length - 1].fields.BROADCAST_OPTION[0], 'broadcast-name--1');
|
||||
|
||||
// Check that the new IDs do not look like array indexes as their enumeration
|
||||
// order could cause unexpected behavior in other places.
|
||||
for (const newBlockId of Object.keys(newBlocks)) {
|
||||
// The actual definition of an array index is: https://tc39.es/ecma262/#array-index
|
||||
// This approximation is currently good enough
|
||||
if (!Number.isNaN(+newBlockId)) {
|
||||
t.fail(`${newBlockId} might be treated as an array index`);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
57
scratch-vm/test/unit/tw_costume_and_sound_inputs.js
Normal file
57
scratch-vm/test/unit/tw_costume_and_sound_inputs.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const {test} = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
|
||||
const extension = {
|
||||
id: 'costumesoundtest',
|
||||
name: 'Costume & Sound',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'costume',
|
||||
text: 'costume [a] [b]',
|
||||
arguments: {
|
||||
a: {
|
||||
type: ArgumentType.COSTUME
|
||||
},
|
||||
b: {
|
||||
type: ArgumentType.COSTUME,
|
||||
defaultValue: 'default costume'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
blockType: BlockType.COMMAND,
|
||||
opcode: 'sound',
|
||||
text: 'sound [a] [b]',
|
||||
arguments: {
|
||||
a: {
|
||||
type: ArgumentType.SOUND
|
||||
},
|
||||
b: {
|
||||
type: ArgumentType.SOUND,
|
||||
defaultValue: 'default sound'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
test('COSTUME and SOUND inputs generate correct scratch-blocks XML', t => {
|
||||
const rt = new Runtime();
|
||||
rt.on('EXTENSION_ADDED', info => {
|
||||
/* eslint-disable max-len */
|
||||
t.equal(
|
||||
info.blocks[0].xml,
|
||||
'<block type="costumesoundtest_costume"><value name="a"><shadow type="looks_costume"></shadow></value><value name="b"><shadow type="looks_costume"><field name="COSTUME">default costume</field></shadow></value></block>'
|
||||
);
|
||||
t.equal(
|
||||
info.blocks[1].xml,
|
||||
'<block type="costumesoundtest_sound"><value name="a"><shadow type="sound_sounds_menu"></shadow></value><value name="b"><shadow type="sound_sounds_menu"><field name="SOUND_MENU">default sound</field></shadow></value></block>'
|
||||
);
|
||||
/* eslint-enable max-len */
|
||||
t.end();
|
||||
});
|
||||
rt._registerExtensionPrimitives(extension);
|
||||
});
|
||||
76
scratch-vm/test/unit/tw_costume_import_export.js
Normal file
76
scratch-vm/test/unit/tw_costume_import_export.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const {
|
||||
parseVectorMetadata,
|
||||
exportCostume
|
||||
} = require('../../src/serialization/tw-costume-import-export');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('parseVectorMetadata', t => {
|
||||
/* eslint-disable max-len */
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:0:0-->'),
|
||||
[0, 0]
|
||||
);
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:-0.0:-0.0-->'),
|
||||
[0, 0]
|
||||
);
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:-1:3-->'),
|
||||
[-1, 3]
|
||||
);
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:106.62300344745225:-11.822572945859918-->'),
|
||||
[106.62300344745225, -11.822572945859918]
|
||||
);
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:a:b-->'),
|
||||
null
|
||||
);
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg><!--rotationCenter:-1:-->'),
|
||||
null
|
||||
);
|
||||
t.same(
|
||||
parseVectorMetadata('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg>'),
|
||||
null
|
||||
);
|
||||
/* eslint-enable max-len */
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('exportCostume', t => {
|
||||
// PNG and JPG costumes are exported as-is
|
||||
t.same(exportCostume({
|
||||
dataFormat: 'png',
|
||||
asset: {
|
||||
data: new Uint8Array([10, 20, 30])
|
||||
}
|
||||
}), new Uint8Array([10, 20, 30]));
|
||||
t.same(exportCostume({
|
||||
dataFormat: 'jpg',
|
||||
asset: {
|
||||
data: new Uint8Array([40, 50, 60])
|
||||
}
|
||||
}), new Uint8Array([40, 50, 60]));
|
||||
|
||||
t.same(exportCostume({
|
||||
dataFormat: 'svg',
|
||||
asset: {
|
||||
data: new TextEncoder().encode('<svg></svg>')
|
||||
},
|
||||
rotationCenterX: 89.339393,
|
||||
rotationCenterY: -3.7373
|
||||
}), new TextEncoder().encode('<svg></svg><!--rotationCenter:89.339393:-3.7373-->'));
|
||||
|
||||
t.same(exportCostume({
|
||||
dataFormat: 'svg',
|
||||
asset: {
|
||||
data: new TextEncoder().encode('<svg></svg><!--rotationCenter:78.23:-9-->')
|
||||
},
|
||||
rotationCenterX: 89.339393,
|
||||
rotationCenterY: -3.7373
|
||||
}), new TextEncoder().encode('<svg></svg><!--rotationCenter:89.339393:-3.7373-->'));
|
||||
|
||||
t.end();
|
||||
});
|
||||
34
scratch-vm/test/unit/tw_extension_api_common.js
Normal file
34
scratch-vm/test/unit/tw_extension_api_common.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const ScratchCommon = require('../../src/extension-support/tw-extension-api-common');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('ArgumentType', t => {
|
||||
t.equal(ScratchCommon.ArgumentType.ANGLE, 'angle');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('BlockType', t => {
|
||||
t.equal(ScratchCommon.BlockType.BOOLEAN, 'Boolean');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('TargetType', t => {
|
||||
t.equal(ScratchCommon.TargetType.SPRITE, 'sprite');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Cast', t => {
|
||||
// Cast is thoroughly tested elsewhere. We just want to make sure that the public methods
|
||||
// don't get deleted unexpectedly.
|
||||
t.equal(ScratchCommon.Cast.toNumber('5'), 5);
|
||||
t.equal(ScratchCommon.Cast.toBoolean('true'), true);
|
||||
t.equal(ScratchCommon.Cast.toString('something'), 'something');
|
||||
t.same(ScratchCommon.Cast.toRgbColorList('#abcdef'), [0xab, 0xcd, 0xef]);
|
||||
t.same(ScratchCommon.Cast.toRgbColorObject('#abcdef'), {r: 0xab, g: 0xcd, b: 0xef});
|
||||
t.equal(ScratchCommon.Cast.isWhiteSpace(''), true);
|
||||
t.equal(ScratchCommon.Cast.compare(1, 2), -1);
|
||||
t.equal(ScratchCommon.Cast.isInt(5.5), false);
|
||||
t.type(ScratchCommon.Cast.LIST_INVALID, 'string');
|
||||
t.type(ScratchCommon.Cast.LIST_ALL, 'string');
|
||||
t.equal(ScratchCommon.Cast.toListIndex('1.5', 10, false), 1);
|
||||
t.end();
|
||||
});
|
||||
80
scratch-vm/test/unit/tw_extension_manager.js
Normal file
80
scratch-vm/test/unit/tw_extension_manager.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const {test} = require('tap');
|
||||
const ExtensionManager = require('../../src/extension-support/extension-manager');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
|
||||
test('isBuiltinExtension', t => {
|
||||
const fakeRuntime = {};
|
||||
const manager = new ExtensionManager(fakeRuntime);
|
||||
t.equal(manager.isBuiltinExtension('pen'), true);
|
||||
t.equal(manager.isBuiltinExtension('lksdfjlskdf'), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('_isValidExtensionURL', t => {
|
||||
const fakeRuntime = {};
|
||||
const manager = new ExtensionManager(fakeRuntime);
|
||||
t.equal(manager._isValidExtensionURL('fetch'), false);
|
||||
t.equal(manager._isValidExtensionURL(''), false);
|
||||
t.equal(manager._isValidExtensionURL('extensions.turbowarp.org/fetch.js'), false);
|
||||
t.equal(manager._isValidExtensionURL('https://extensions.turbowarp.org/fetch.js'), true);
|
||||
t.equal(manager._isValidExtensionURL('http://extensions.turbowarp.org/fetch.js'), true);
|
||||
t.equal(manager._isValidExtensionURL('http://localhost:8000'), true);
|
||||
t.equal(manager._isValidExtensionURL('data:application/javascript;base64,YWxlcnQoMSk='), true);
|
||||
t.equal(manager._isValidExtensionURL('file:///home/test/extension.js'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('loadExtensionURL, getExtensionURLs, deduplication', async t => {
|
||||
const vm = new VM();
|
||||
|
||||
let loadedExtensions = 0;
|
||||
vm.extensionManager.securityManager.getSandboxMode = () => 'unsandboxed';
|
||||
global.document = {
|
||||
createElement: () => {
|
||||
loadedExtensions++;
|
||||
const element = {};
|
||||
setTimeout(() => {
|
||||
global.Scratch.extensions.register({
|
||||
getInfo: () => ({
|
||||
id: `extension${loadedExtensions}`
|
||||
})
|
||||
});
|
||||
});
|
||||
return element;
|
||||
},
|
||||
body: {
|
||||
appendChild: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
const url1 = 'https://turbowarp.org/1.js';
|
||||
t.equal(vm.extensionManager.isExtensionURLLoaded(url1), false);
|
||||
t.same(vm.extensionManager.getExtensionURLs(), {});
|
||||
await vm.extensionManager.loadExtensionURL(url1);
|
||||
t.equal(vm.extensionManager.isExtensionURLLoaded(url1), true);
|
||||
t.equal(loadedExtensions, 1);
|
||||
t.same(vm.extensionManager.getExtensionURLs(), {
|
||||
extension1: url1
|
||||
});
|
||||
|
||||
// Loading the extension again should do nothing.
|
||||
await vm.extensionManager.loadExtensionURL(url1);
|
||||
t.equal(vm.extensionManager.isExtensionURLLoaded(url1), true);
|
||||
t.equal(loadedExtensions, 1);
|
||||
t.same(vm.extensionManager.getExtensionURLs(), {
|
||||
extension1: url1
|
||||
});
|
||||
|
||||
// Loading another extension should work
|
||||
const url2 = 'https://turbowarp.org/2.js';
|
||||
t.equal(vm.extensionManager.isExtensionURLLoaded(url2), false);
|
||||
await vm.extensionManager.loadExtensionURL(url2);
|
||||
t.equal(vm.extensionManager.isExtensionURLLoaded(url2), true);
|
||||
t.equal(loadedExtensions, 2);
|
||||
t.same(vm.extensionManager.getExtensionURLs(), {
|
||||
extension1: url1,
|
||||
extension2: url2
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
63
scratch-vm/test/unit/tw_extension_monitors.js
Normal file
63
scratch-vm/test/unit/tw_extension_monitors.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const {test} = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
|
||||
test('Boolean blocks can be monitors', t => {
|
||||
const rt = new Runtime();
|
||||
rt._registerExtensionPrimitives({
|
||||
id: 'testextension',
|
||||
blocks: [
|
||||
{
|
||||
blockType: BlockType.REPORTER,
|
||||
opcode: 'reporter1',
|
||||
text: 'reporter 1'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.REPORTER,
|
||||
opcode: 'reporter2',
|
||||
text: 'reporter 2',
|
||||
disableMonitor: true
|
||||
},
|
||||
{
|
||||
blockType: BlockType.REPORTER,
|
||||
opcode: 'reporter3',
|
||||
text: 'reporter 3 [INPUT]',
|
||||
arguments: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
blockType: BlockType.BOOLEAN,
|
||||
opcode: 'boolean1',
|
||||
text: 'boolean 1'
|
||||
},
|
||||
{
|
||||
blockType: BlockType.BOOLEAN,
|
||||
opcode: 'boolean2',
|
||||
text: 'boolean 2',
|
||||
disableMonitor: true
|
||||
},
|
||||
{
|
||||
blockType: BlockType.BOOLEAN,
|
||||
opcode: 'boolean3',
|
||||
text: 'boolean 3 [INPUT]',
|
||||
arguments: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const json = rt.getBlocksJSON();
|
||||
t.equal(json.length, 6);
|
||||
t.equal(json[0].checkboxInFlyout, true);
|
||||
t.equal(json[1].checkboxInFlyout, undefined);
|
||||
t.equal(json[2].checkboxInFlyout, undefined);
|
||||
t.equal(json[3].checkboxInFlyout, true);
|
||||
t.equal(json[4].checkboxInFlyout, undefined);
|
||||
t.equal(json[5].checkboxInFlyout, undefined);
|
||||
t.end();
|
||||
});
|
||||
27
scratch-vm/test/unit/tw_extension_music.js
Normal file
27
scratch-vm/test/unit/tw_extension_music.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const test = require('tap').test;
|
||||
const Music = require('../../src/extensions/scratch3_music/index.js');
|
||||
const Runtime = require('../../src/engine/runtime.js');
|
||||
|
||||
test('_isConcurrencyLimited', t => {
|
||||
const rt = new Runtime();
|
||||
|
||||
// sanity check so that the setRuntimeOptions() call below actually does something
|
||||
t.equal(rt.runtimeOptions.miscLimits, true, 'misc limits enabled by default');
|
||||
|
||||
const blocks = new Music(rt);
|
||||
t.equal(blocks._isConcurrencyLimited(), false, 'not limited initially');
|
||||
|
||||
// logic here is slightly weird but this matches Scratch
|
||||
blocks._concurrencyCounter = Music.CONCURRENCY_LIMIT;
|
||||
t.equal(blocks._isConcurrencyLimited(), false, 'not limited at limit');
|
||||
|
||||
blocks._concurrencyCounter = Music.CONCURRENCY_LIMIT + 1;
|
||||
t.equal(blocks._isConcurrencyLimited(), true, 'limited above limit');
|
||||
|
||||
rt.setRuntimeOptions({
|
||||
miscLimits: false
|
||||
});
|
||||
t.equal(blocks._isConcurrencyLimited(), false, 'not limited when miscLimits: false');
|
||||
|
||||
t.end();
|
||||
});
|
||||
76
scratch-vm/test/unit/tw_jsexecute.js
Normal file
76
scratch-vm/test/unit/tw_jsexecute.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const {test} = require('tap');
|
||||
const jsexecute = require('../../src/compiler/jsexecute');
|
||||
const Cast = require('../../src/util/cast');
|
||||
const {stringify} = require('@turbowarp/json');
|
||||
|
||||
const evaluateRuntimeFunction = functionName => jsexecute.scopedEval(functionName);
|
||||
|
||||
test('runtimeFunctions are valid', t => {
|
||||
for (const functionName of Object.keys(jsexecute.runtimeFunctions)) {
|
||||
const fn = evaluateRuntimeFunction(functionName);
|
||||
t.type(fn, 'function', `${functionName} is function`);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('all runtimeFunctions can be used together', t => {
|
||||
const script = Object.keys(jsexecute.runtimeFunctions).join(';');
|
||||
jsexecute.scopedEval(script);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('comparison functions are equivalent to Cast.compare', t => {
|
||||
const VALUES = [
|
||||
0,
|
||||
-0,
|
||||
1,
|
||||
'0',
|
||||
'',
|
||||
'.',
|
||||
true,
|
||||
false,
|
||||
'true',
|
||||
'false',
|
||||
'true ',
|
||||
'apple',
|
||||
'Apple',
|
||||
'Apple ',
|
||||
' 123',
|
||||
' 123.0',
|
||||
'+123.5',
|
||||
123,
|
||||
0.23,
|
||||
'0.23',
|
||||
'.23',
|
||||
'-.23',
|
||||
'0.0',
|
||||
NaN,
|
||||
'NaN',
|
||||
Infinity,
|
||||
-Infinity,
|
||||
'Infinity',
|
||||
'-Infinity',
|
||||
'\t',
|
||||
'\r\n\u00a0'
|
||||
];
|
||||
const compareEqual = evaluateRuntimeFunction('compareEqual');
|
||||
const compareGreaterThan = evaluateRuntimeFunction('compareGreaterThan');
|
||||
const compareLessThan = evaluateRuntimeFunction('compareLessThan');
|
||||
for (const a of VALUES) {
|
||||
for (const b of VALUES) {
|
||||
// Because there are so many tests, calling t.ok() each time is actually quite slow,
|
||||
// so only call into tap when something failed.
|
||||
const cast = Cast.compare(a, b);
|
||||
if (compareEqual(a, b) !== (cast === 0)) {
|
||||
t.fail(`${stringify(a)} should be === ${stringify(b)}`);
|
||||
}
|
||||
if (compareGreaterThan(a, b) !== (cast > 0)) {
|
||||
t.fail(`${stringify(a)} should be > ${stringify(b)}`);
|
||||
}
|
||||
if (compareLessThan(a, b) !== (cast < 0)) {
|
||||
t.fail(`${stringify(a)} should be < ${stringify(b)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
77
scratch-vm/test/unit/tw_number_argument.js
Normal file
77
scratch-vm/test/unit/tw_number_argument.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const {test} = require('tap');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
|
||||
test('NUMBER argument defaultValue', t => {
|
||||
const runtime = new Runtime();
|
||||
runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
|
||||
/* eslint-disable max-len */
|
||||
t.equal(
|
||||
categoryInfo.blocks[0].xml,
|
||||
'<block type="testextension_testNone"><value name="a"><shadow type="math_number"></shadow></value></block>'
|
||||
);
|
||||
t.equal(
|
||||
categoryInfo.blocks[1].xml,
|
||||
'<block type="testextension_testEmptyString"><value name="a"><shadow type="math_number"><field name="NUM"></field></shadow></value></block>'
|
||||
);
|
||||
t.equal(
|
||||
categoryInfo.blocks[2].xml,
|
||||
'<block type="testextension_testZeroString"><value name="a"><shadow type="math_number"><field name="NUM">0</field></shadow></value></block>'
|
||||
);
|
||||
t.equal(
|
||||
categoryInfo.blocks[3].xml,
|
||||
'<block type="testextension_testZeroNumber"><value name="a"><shadow type="math_number"><field name="NUM">0</field></shadow></value></block>'
|
||||
);
|
||||
/* eslint-enable max-len */
|
||||
t.end();
|
||||
});
|
||||
runtime._registerExtensionPrimitives({
|
||||
id: 'testextension',
|
||||
blocks: [
|
||||
{
|
||||
type: BlockType.COMMAND,
|
||||
opcode: 'testNone',
|
||||
text: 'block [a]',
|
||||
arguments: {
|
||||
a: {
|
||||
type: ArgumentType.NUMBER
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: BlockType.COMMAND,
|
||||
opcode: 'testEmptyString',
|
||||
text: 'block [a]',
|
||||
arguments: {
|
||||
a: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: BlockType.COMMAND,
|
||||
opcode: 'testZeroString',
|
||||
text: 'block [a]',
|
||||
arguments: {
|
||||
a: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: BlockType.COMMAND,
|
||||
opcode: 'testZeroNumber',
|
||||
text: 'block [a]',
|
||||
arguments: {
|
||||
a: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
19
scratch-vm/test/unit/tw_performance_measure_error.js
Normal file
19
scratch-vm/test/unit/tw_performance_measure_error.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const test = require('tap').test;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
global.performance = {
|
||||
mark () {
|
||||
// No-op
|
||||
},
|
||||
measure () {
|
||||
throw new Error('Mock error to simulate browser garbage collecting one of the marks before this code runs');
|
||||
}
|
||||
};
|
||||
|
||||
test('performance.measure() error in loadProject is ignored', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
await vm.loadProject(fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'tw-empty-project.sb3')));
|
||||
t.end();
|
||||
});
|
||||
143
scratch-vm/test/unit/tw_sandboxed_extensions.js
Normal file
143
scratch-vm/test/unit/tw_sandboxed_extensions.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const {test} = require('tap');
|
||||
|
||||
global.Request = class {
|
||||
constructor (url) {
|
||||
this.url = url;
|
||||
}
|
||||
};
|
||||
global.fetch = (url, options = {}) => (
|
||||
Promise.resolve(`[Response ${url instanceof Request ? url.url : url} options=${JSON.stringify(options)}]`)
|
||||
);
|
||||
|
||||
// Remove navigator object from Node 21 and later
|
||||
delete global.navigator;
|
||||
|
||||
// Need to trick the extension API to think it's running in a worker
|
||||
// It will not actually use this object ever.
|
||||
global.self = {};
|
||||
// This will install extension worker APIs onto `global`
|
||||
require('../../src/extension-support/extension-worker');
|
||||
|
||||
test('basic API', t => {
|
||||
t.type(global.Scratch.extensions.register, 'function');
|
||||
t.equal(global.Scratch.ArgumentType.BOOLEAN, 'Boolean');
|
||||
t.equal(global.Scratch.BlockType.REPORTER, 'reporter');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('not unsandboxed', t => {
|
||||
t.not(global.Scratch.extensions.unsandboxed, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Cast', t => {
|
||||
// Cast is thoroughly tested elsewhere
|
||||
t.equal(global.Scratch.Cast.toString(5), '5');
|
||||
t.equal(global.Scratch.Cast.toNumber(' 5'), 5);
|
||||
t.equal(global.Scratch.Cast.toBoolean('true'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fetch', async t => {
|
||||
t.equal(await global.Scratch.canFetch('https://untrusted.example/'), true);
|
||||
t.equal(await global.Scratch.fetch('https://untrusted.example/'), '[Response https://untrusted.example/ options={}]');
|
||||
t.equal(await global.Scratch.fetch('https://untrusted.example/', {
|
||||
method: 'POST'
|
||||
}), `[Response https://untrusted.example/ options={"method":"POST"}]`);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('openWindow', async t => {
|
||||
t.equal(await global.Scratch.canOpenWindow('https://example.com/'), false);
|
||||
await t.rejects(global.Scratch.openWindow('https://example.com/'), /^Scratch\.openWindow not supported in sandboxed extensions$/);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('redirect', async t => {
|
||||
t.equal(await global.Scratch.canRedirect('https://example.com/'), false);
|
||||
await t.rejects(global.Scratch.redirect('https://example.com/'), /^Scratch\.redirect not supported in sandboxed extensions$/);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('translate', t => {
|
||||
t.equal(global.Scratch.translate({
|
||||
id: 'test1',
|
||||
default: 'Message 1: {var}',
|
||||
description: 'Description'
|
||||
}, {
|
||||
var: 'test'
|
||||
}), 'Message 1: test');
|
||||
t.equal(global.Scratch.translate('test1'), 'test1');
|
||||
t.equal(global.Scratch.translate('test1 {VAR}', {
|
||||
VAR: '3'
|
||||
}), 'test1 3');
|
||||
t.equal(global.Scratch.translate.language, 'en');
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
test1: 'EN Message 1: {var}'
|
||||
},
|
||||
es: {
|
||||
test1: 'ES Message 1: {var}'
|
||||
}
|
||||
};
|
||||
|
||||
// Should default to English when no navigator object
|
||||
global.Scratch.translate.setup(messages);
|
||||
t.equal(global.Scratch.translate({
|
||||
id: 'test1',
|
||||
default: 'Message 1',
|
||||
description: 'Description'
|
||||
}, {
|
||||
var: 'ok'
|
||||
}), 'EN Message 1: ok');
|
||||
t.equal(global.Scratch.translate.language, 'en');
|
||||
|
||||
// But if there is a navigator object, it should use its language.
|
||||
global.navigator = {
|
||||
language: 'es'
|
||||
};
|
||||
// Note that real extensions will only generally call setup() once, but we need to do this
|
||||
// again so that it realizes the language changed.
|
||||
global.Scratch.translate.setup(messages);
|
||||
t.equal(global.Scratch.translate({
|
||||
id: 'test1',
|
||||
default: 'Message 1',
|
||||
description: 'Description'
|
||||
}, {
|
||||
var: 'ok'
|
||||
}), 'ES Message 1: ok');
|
||||
t.equal(global.Scratch.translate.language, 'es');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canRecordAudio', async t => {
|
||||
t.equal(await global.Scratch.canRecordAudio(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canRecordVideo', async t => {
|
||||
t.equal(await global.Scratch.canRecordVideo(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canReadClipboard', async t => {
|
||||
t.equal(await global.Scratch.canReadClipboard(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canNotify', async t => {
|
||||
t.equal(await global.Scratch.canNotify(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canGeolocate', async t => {
|
||||
t.equal(await global.Scratch.canGeolocate(), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canEmbed', async t => {
|
||||
t.equal(await global.Scratch.canEmbed('https://example.com/'), false);
|
||||
t.end();
|
||||
});
|
||||
286
scratch-vm/test/unit/tw_scratchx.js
Normal file
286
scratch-vm/test/unit/tw_scratchx.js
Normal file
@@ -0,0 +1,286 @@
|
||||
const ScratchXUtilities = require('../../src/extension-support/tw-scratchx-utilities');
|
||||
const createScratchX = require('../../src/extension-support/tw-scratchx-compatibility-layer');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('argument index to id', t => {
|
||||
t.equal(ScratchXUtilities.argumentIndexToId(0), '0');
|
||||
t.equal(ScratchXUtilities.argumentIndexToId(1), '1');
|
||||
t.equal(ScratchXUtilities.argumentIndexToId(2), '2');
|
||||
t.equal(ScratchXUtilities.argumentIndexToId(3), '3');
|
||||
t.equal(ScratchXUtilities.argumentIndexToId(39), '39');
|
||||
t.equal(ScratchXUtilities.argumentIndexToId(1000000000), '1000000000');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('generate extension id', t => {
|
||||
t.equal(ScratchXUtilities.generateExtensionId('Spotify'), 'sbxspotify');
|
||||
t.equal(ScratchXUtilities.generateExtensionId('Spo _t ify'), 'sbxspotify');
|
||||
t.equal(ScratchXUtilities.generateExtensionId('Spo _t $#@! 3ify😮'), 'sbxspot3ify');
|
||||
t.end();
|
||||
});
|
||||
|
||||
const mockScratchExtensions = () => {
|
||||
const mockScratch = {};
|
||||
return createScratchX(mockScratch);
|
||||
};
|
||||
|
||||
const convert = (...args) => {
|
||||
let registered = null;
|
||||
const mockScratch = {
|
||||
extensions: {
|
||||
register: extensionObject => {
|
||||
if (registered) {
|
||||
// In tests we don't want this
|
||||
throw new Error('register() called twice');
|
||||
}
|
||||
registered = extensionObject;
|
||||
}
|
||||
}
|
||||
};
|
||||
const ScratchExtensions = createScratchX(mockScratch);
|
||||
ScratchExtensions.register(...args);
|
||||
if (!registered) {
|
||||
throw new Error('Did not register()');
|
||||
}
|
||||
return registered;
|
||||
};
|
||||
|
||||
test('register', t => {
|
||||
const ScratchExtensions = mockScratchExtensions();
|
||||
t.type(ScratchExtensions.register, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('complex extension', async t => {
|
||||
let stepsMoved = 0;
|
||||
const moveSteps = n => {
|
||||
stepsMoved += n;
|
||||
};
|
||||
|
||||
let doNothingCalled = false;
|
||||
const doNothing = () => {
|
||||
doNothingCalled = true;
|
||||
};
|
||||
|
||||
const fetch = (url, callback) => {
|
||||
callback(`Fetched: ${url}`);
|
||||
return 'This value should be ignored.';
|
||||
};
|
||||
|
||||
const multiplyAndAppend = (a, b, c) => `${a * b}${c}`;
|
||||
|
||||
const repeat = (string, count, callback) => {
|
||||
callback(string.repeat(count));
|
||||
return 'This value should be ignored.';
|
||||
};
|
||||
|
||||
const touching = (sprite, bool) => sprite === 'Sprite9' && bool === true;
|
||||
|
||||
const converted = convert(
|
||||
'My Extension',
|
||||
{
|
||||
blocks: [
|
||||
['', 'move %n steps', 'moveSteps', 50],
|
||||
[' ', 'do nothing', 'doNothing', 100, 200],
|
||||
['w', 'fetch %m:urls1', 'fetch'],
|
||||
[' '],
|
||||
['r', 'multiply %n by %n and append %s', 'multiplyAndAppend'],
|
||||
['R', 'repeat %m.myMenu %n', 'repeat', ''],
|
||||
['-'],
|
||||
['b', 'touching %s %b', 'touching', 'Sprite1', 'ignored']
|
||||
],
|
||||
menus: {
|
||||
myMenu: ['abc', 'def', 123, true, false],
|
||||
urls1: ['https://example.com/', 'https://example.org/']
|
||||
},
|
||||
url: 'https://turbowarp.org/myextensiondocs.html'
|
||||
},
|
||||
{
|
||||
unusedGarbage: 10,
|
||||
moveSteps,
|
||||
doNothing,
|
||||
fetch,
|
||||
multiplyAndAppend,
|
||||
repeat,
|
||||
touching
|
||||
}
|
||||
);
|
||||
|
||||
const info = converted.getInfo();
|
||||
t.equal(info.id, 'sbxmyextension');
|
||||
t.equal(info.docsURI, 'https://turbowarp.org/myextensiondocs.html');
|
||||
|
||||
t.same(info.blocks, [
|
||||
{
|
||||
opcode: 'moveSteps',
|
||||
text: 'move [0] steps',
|
||||
blockType: 'command',
|
||||
arguments: [
|
||||
{
|
||||
type: 'number',
|
||||
defaultValue: 50
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
opcode: 'doNothing',
|
||||
text: 'do nothing',
|
||||
blockType: 'command',
|
||||
arguments: []
|
||||
},
|
||||
{
|
||||
opcode: 'fetch',
|
||||
text: 'fetch [0]',
|
||||
blockType: 'command',
|
||||
arguments: [
|
||||
{
|
||||
type: 'string',
|
||||
menu: 'urls1'
|
||||
}
|
||||
]
|
||||
},
|
||||
'---',
|
||||
{
|
||||
opcode: 'multiplyAndAppend',
|
||||
text: 'multiply [0] by [1] and append [2]',
|
||||
blockType: 'reporter',
|
||||
arguments: [
|
||||
{
|
||||
type: 'number',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
opcode: 'repeat',
|
||||
text: 'repeat [0] [1]',
|
||||
blockType: 'reporter',
|
||||
arguments: [
|
||||
{
|
||||
type: 'string',
|
||||
menu: 'myMenu',
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
defaultValue: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
'---',
|
||||
{
|
||||
opcode: 'touching',
|
||||
text: 'touching [0] [1]',
|
||||
blockType: 'Boolean',
|
||||
arguments: [
|
||||
{
|
||||
type: 'string',
|
||||
defaultValue: 'Sprite1'
|
||||
},
|
||||
{
|
||||
type: 'Boolean'
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
t.same(info.menus, {
|
||||
myMenu: {
|
||||
items: ['abc', 'def', 123, true, false]
|
||||
},
|
||||
urls1: {
|
||||
items: ['https://example.com/', 'https://example.org/']
|
||||
}
|
||||
});
|
||||
|
||||
// Now let's make sure that the converter has properly wrapped our functions.
|
||||
t.equal(stepsMoved, 0);
|
||||
t.equal(converted.moveSteps({
|
||||
0: 30
|
||||
}), undefined);
|
||||
t.equal(stepsMoved, 30);
|
||||
|
||||
t.equal(doNothingCalled, false);
|
||||
t.equal(converted.doNothing({}), undefined);
|
||||
t.equal(doNothingCalled, true);
|
||||
|
||||
t.type(converted.fetch({
|
||||
0: 'https://example.com/'
|
||||
}).then, 'function');
|
||||
t.equal(await converted.fetch({
|
||||
0: 'https://example.com/'
|
||||
}), 'Fetched: https://example.com/');
|
||||
|
||||
t.equal(converted.multiplyAndAppend({
|
||||
0: 31,
|
||||
1: 7,
|
||||
2: 'Cat'
|
||||
}), '217Cat');
|
||||
|
||||
t.type(converted.repeat({
|
||||
0: '',
|
||||
1: 0
|
||||
}).then, 'function');
|
||||
t.equal(await converted.repeat({
|
||||
0: 'scratchx',
|
||||
1: 3
|
||||
}), 'scratchxscratchxscratchx');
|
||||
|
||||
t.equal(converted.touching({
|
||||
0: 'Sprite1',
|
||||
1: true
|
||||
}), false);
|
||||
t.equal(converted.touching({
|
||||
0: 'Sprite9',
|
||||
1: true
|
||||
}), true);
|
||||
t.equal(converted.touching({
|
||||
0: 'Sprite9',
|
||||
1: false
|
||||
}), false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('display name', t => {
|
||||
const converted = convert(
|
||||
'Internal Name',
|
||||
{
|
||||
blocks: [],
|
||||
displayName: 'Display Name'
|
||||
},
|
||||
{
|
||||
|
||||
}
|
||||
);
|
||||
t.equal(converted.getInfo().name, 'Display Name');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('_getStatus', t => {
|
||||
const _getStatus = () => ({
|
||||
status: 2,
|
||||
msg: 'Ready'
|
||||
});
|
||||
const converted = convert(
|
||||
'Name',
|
||||
{
|
||||
blocks: []
|
||||
},
|
||||
{
|
||||
_getStatus: _getStatus,
|
||||
unusedProperty: 10
|
||||
}
|
||||
);
|
||||
t.equal(converted._getStatus, _getStatus);
|
||||
t.equal('unusedProperty' in converted, false);
|
||||
t.end();
|
||||
});
|
||||
38
scratch-vm/test/unit/tw_static_fetch.js
Normal file
38
scratch-vm/test/unit/tw_static_fetch.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const {test} = require('tap');
|
||||
const staticFetch = require('../../src/util/tw-static-fetch');
|
||||
|
||||
test('fetch simple base64', t => {
|
||||
const res = staticFetch('data:text/plain;base64,VGVzdGluZyB0ZXN0aW5nIDEyMw==');
|
||||
res.text().then(text => {
|
||||
t.equal(text, 'Testing testing 123');
|
||||
t.equal(res.status, 200);
|
||||
t.equal(res.ok, true);
|
||||
t.equal(res.headers.get('content-type'), 'text/plain');
|
||||
t.equal(res.headers.get('content-length'), '19');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch base64 with all possible bytes', t => {
|
||||
// eslint-disable-next-line max-len
|
||||
const res = staticFetch('Data:Application/Octet-Stream;BASE64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==');
|
||||
res.arrayBuffer().then(buffer => {
|
||||
t.same(Array.from(new Uint8Array(buffer)), Array(256)
|
||||
.fill()
|
||||
.map((_, index) => index)
|
||||
);
|
||||
t.equal(res.headers.get('content-type'), 'application/octet-stream');
|
||||
t.equal(res.headers.get('content-length'), '256');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch not data:', t => {
|
||||
t.equal(staticFetch('blob:https://turbowarp.org/54346944-16cf-4ce9-aed4-e1df8ad0d779'), null);
|
||||
t.equal(staticFetch('https://example.com/'), null);
|
||||
t.equal(staticFetch('http://example.com/'), null);
|
||||
t.equal(staticFetch('file:///etc/hosts'), null);
|
||||
t.equal(staticFetch('oegirjdf'), null);
|
||||
t.equal(staticFetch(''), null);
|
||||
t.end();
|
||||
});
|
||||
52
scratch-vm/test/unit/tw_stop_this_script.js
Normal file
52
scratch-vm/test/unit/tw_stop_this_script.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const {test} = require('tap');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Target = require('../../src/engine/target');
|
||||
|
||||
test('stopThisScript procedures_call reporter form', t => {
|
||||
const rt = new Runtime();
|
||||
const target = new Target(rt, null);
|
||||
|
||||
target.blocks.createBlock({
|
||||
id: 'reporterCall',
|
||||
opcode: 'procedures_call',
|
||||
inputs: {},
|
||||
fields: {},
|
||||
mutation: {
|
||||
return: '1'
|
||||
},
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
parent: null,
|
||||
next: 'afterReporterCall'
|
||||
});
|
||||
target.blocks.createBlock({
|
||||
id: 'afterReporterCall',
|
||||
opcode: 'motion_ifonedgebounce',
|
||||
inputs: {},
|
||||
fields: {},
|
||||
mutation: null,
|
||||
shadow: false,
|
||||
topLevel: false,
|
||||
parent: null,
|
||||
next: null
|
||||
});
|
||||
|
||||
const thread = new Thread('reporterCall');
|
||||
thread.target = target;
|
||||
|
||||
// pretend to run reporterCall
|
||||
thread.pushStack('reporterCall');
|
||||
thread.peekStackFrame().waitingReporter = true;
|
||||
|
||||
// pretend to run scripts inside of the procedure
|
||||
thread.pushStack('fakeBlock');
|
||||
|
||||
// stopping or returning should always return to reporterCall, not the block after
|
||||
thread.stopThisScript();
|
||||
t.same(thread.stack, [
|
||||
'reporterCall'
|
||||
]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
100
scratch-vm/test/unit/tw_stored_settings.js
Normal file
100
scratch-vm/test/unit/tw_stored_settings.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const tap = require('tap');
|
||||
const path = require('path');
|
||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
const makeVM = () => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
return vm;
|
||||
};
|
||||
|
||||
for (const file of ['empty-comment.sb3', 'no-comment.sb3']) {
|
||||
test(`serializes and deserializes settings (${file})`, t => {
|
||||
const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/${file}`));
|
||||
const vm = makeVM();
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.setFramerate(45);
|
||||
vm.setTurboMode(true);
|
||||
vm.setInterpolation(true);
|
||||
vm.setRuntimeOptions({
|
||||
maxClones: Infinity,
|
||||
miscLimits: false,
|
||||
fencing: false
|
||||
});
|
||||
vm.setStageSize(100, 101);
|
||||
vm.storeProjectOptions();
|
||||
|
||||
const newVM = makeVM();
|
||||
newVM.loadProject(vm.toJSON())
|
||||
.then(() => {
|
||||
t.equal(newVM.runtime.framerate, vm.runtime.framerate);
|
||||
t.equal(newVM.runtime.turboMode, vm.runtime.turboMode);
|
||||
t.same(newVM.runtime.runtimeOptions, vm.runtime.runtimeOptions);
|
||||
t.equal(newVM.runtime.interpolationEnabled, vm.runtime.interpolationEnabled);
|
||||
t.equal(newVM.runtime.stageWidth, vm.runtime.stageWidth);
|
||||
t.equal(newVM.runtime.stageHeight, vm.runtime.stageHeight);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('Reuses comment if it already exists', t => {
|
||||
const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/empty-comment.sb3`));
|
||||
const vm = makeVM();
|
||||
vm.loadProject(project)
|
||||
.then(() => {
|
||||
t.equal(Object.keys(vm.runtime.getTargetForStage().comments).length, 1);
|
||||
vm.setFramerate(99);
|
||||
vm.storeProjectOptions();
|
||||
t.equal(Object.keys(vm.runtime.getTargetForStage().comments).length, 1);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Storing settings emits workspace update only when stage open', t => {
|
||||
const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/sprite.sb3`));
|
||||
const vm = makeVM();
|
||||
vm.loadProject(project)
|
||||
.then(() => {
|
||||
let didFireUpdate = false;
|
||||
vm.on('workspaceUpdate', () => {
|
||||
didFireUpdate = true;
|
||||
});
|
||||
vm.storeProjectOptions();
|
||||
t.equal(didFireUpdate, false);
|
||||
vm.setEditingTarget(vm.runtime.getTargetForStage().id);
|
||||
vm.storeProjectOptions();
|
||||
t.equal(didFireUpdate, true);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Storing settings emits project changed', t => {
|
||||
const project = readFileToBuffer(path.resolve(__dirname, `../fixtures/tw-stored-settings/sprite.sb3`));
|
||||
const vm = makeVM();
|
||||
vm.loadProject(project)
|
||||
.then(() => {
|
||||
t.plan(1);
|
||||
vm.on('PROJECT_CHANGED', () => {
|
||||
t.pass();
|
||||
});
|
||||
vm.storeProjectOptions();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('Stored turbo mode emits event on VM', async t => {
|
||||
const vm = makeVM();
|
||||
const project = readFileToBuffer(path.resolve(__dirname, '../fixtures/tw-stored-settings/turbo-mode.sb3'));
|
||||
t.plan(1);
|
||||
vm.on('TURBO_MODE_ON', () => {
|
||||
t.pass('emitted TURBO_MODE_ON');
|
||||
});
|
||||
await vm.loadProject(project);
|
||||
t.end();
|
||||
});
|
||||
28
scratch-vm/test/unit/tw_translate.js
Normal file
28
scratch-vm/test/unit/tw_translate.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const {test} = require('tap');
|
||||
|
||||
// Simulate the network being down or filtered
|
||||
// Run this before fetch-with-timeout.js is gets loaded.
|
||||
global.fetch = () => Promise.reject(new Error('Simulated network error'));
|
||||
|
||||
const Scratch3TranslateBlocks = require('../../src/extensions/scratch3_translate/index');
|
||||
|
||||
// Node 21 and later defines a navigator object, but we want to override that for the test
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: {
|
||||
language: 'en-US'
|
||||
}
|
||||
});
|
||||
|
||||
// Translate tries to access AbortController from window, but does not require it to exist.
|
||||
global.window = {};
|
||||
|
||||
test('translate returns original string on network error', t => {
|
||||
t.plan(1);
|
||||
|
||||
const extension = new Scratch3TranslateBlocks();
|
||||
extension.getTranslate({WORDS: 'My message 123123', LANGUAGE: 'es'})
|
||||
.then(message => {
|
||||
t.equal(message, 'My message 123123');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
429
scratch-vm/test/unit/tw_unsandboxed_extensions.js
Normal file
429
scratch-vm/test/unit/tw_unsandboxed_extensions.js
Normal file
@@ -0,0 +1,429 @@
|
||||
const tap = require('tap');
|
||||
const UnsandboxedExtensionRunner = require('../../src/extension-support/tw-unsandboxed-extension-runner');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
// Mock enough of the document API for the extension runner to think it works.
|
||||
// To more accurately test this, we want to make sure that the URLs we pass in are just strings.
|
||||
// We use a bit of hacky state here to make our document mock know what function to run
|
||||
// when a script with a given URL "loads"
|
||||
const scriptCallbacks = new Map();
|
||||
const setScript = (src, callback) => {
|
||||
scriptCallbacks.set(src, callback);
|
||||
};
|
||||
global.document = {
|
||||
createElement: tagName => {
|
||||
if (tagName.toLowerCase() !== 'script') {
|
||||
throw new Error(`Unknown element: ${tagName}`);
|
||||
}
|
||||
return {
|
||||
tagName: 'SCRIPT',
|
||||
src: '',
|
||||
onload: () => {},
|
||||
onerror: () => {}
|
||||
};
|
||||
},
|
||||
body: {
|
||||
appendChild: element => {
|
||||
if (element.tagName === 'SCRIPT') {
|
||||
setTimeout(() => {
|
||||
const callback = scriptCallbacks.get(element.src);
|
||||
if (callback) {
|
||||
callback();
|
||||
element.onload();
|
||||
} else {
|
||||
element.onerror();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mock various DOM APIs for fetching, window opening, redirecting, etc.
|
||||
global.Request = class {
|
||||
constructor (url) {
|
||||
this.url = url;
|
||||
}
|
||||
};
|
||||
global.fetch = (url, options = {}) => (
|
||||
Promise.resolve(`[Response ${url instanceof Request ? url.url : url} options=${JSON.stringify(options)}]`)
|
||||
);
|
||||
global.window = {
|
||||
open: (url, target, features) => `[Window ${url} target=${target || ''} features=${features || ''}]`
|
||||
};
|
||||
|
||||
// Remove navigator object from Node 21 and later
|
||||
delete global.navigator;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
scriptCallbacks.clear();
|
||||
global.location = {
|
||||
href: 'https://example.com/'
|
||||
};
|
||||
});
|
||||
|
||||
const {test} = tap;
|
||||
|
||||
test('basic API', async t => {
|
||||
t.plan(9);
|
||||
const vm = new VirtualMachine();
|
||||
class MyExtension {}
|
||||
setScript('https://turbowarp.org/1.js', () => {
|
||||
t.equal(global.Scratch.vm, vm);
|
||||
t.equal(global.Scratch.renderer, vm.runtime.renderer);
|
||||
t.equal(global.Scratch.extensions.unsandboxed, true);
|
||||
|
||||
// These APIs are tested elsewhere, just make sure they're getting exported
|
||||
t.equal(global.Scratch.ArgumentType.NUMBER, 'number');
|
||||
t.equal(global.Scratch.BlockType.REPORTER, 'reporter');
|
||||
t.equal(global.Scratch.TargetType.SPRITE, 'sprite');
|
||||
t.equal(global.Scratch.Cast.toNumber('3.14'), 3.14);
|
||||
|
||||
global.Scratch.extensions.register(new MyExtension());
|
||||
});
|
||||
const extensions = await UnsandboxedExtensionRunner.load('https://turbowarp.org/1.js', vm);
|
||||
t.equal(extensions.length, 1);
|
||||
t.ok(extensions[0] instanceof MyExtension);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('multiple VMs loading extensions', async t => {
|
||||
const vm1 = new VirtualMachine();
|
||||
const vm2 = new VirtualMachine();
|
||||
|
||||
class Extension1 {}
|
||||
class Extension2 {}
|
||||
|
||||
let api1 = null;
|
||||
setScript('https://turbowarp.org/1.js', async () => {
|
||||
// Even if this extension takes a while to register, we should still have our own
|
||||
// global.Scratch.
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
if (api1) throw new Error('already ran 1');
|
||||
api1 = global.Scratch;
|
||||
global.Scratch.extensions.register(new Extension1());
|
||||
});
|
||||
|
||||
let api2 = null;
|
||||
setScript('https://turbowarp.org/2.js', () => {
|
||||
if (api2) throw new Error('already ran 2');
|
||||
api2 = global.Scratch;
|
||||
global.Scratch.extensions.register(new Extension2());
|
||||
});
|
||||
|
||||
const extensions = await Promise.all([
|
||||
UnsandboxedExtensionRunner.load('https://turbowarp.org/1.js', vm1),
|
||||
UnsandboxedExtensionRunner.load('https://turbowarp.org/2.js', vm2)
|
||||
]);
|
||||
|
||||
t.not(api1, api2);
|
||||
t.type(api1.extensions.register, 'function');
|
||||
t.type(api2.extensions.register, 'function');
|
||||
t.equal(api1.vm, vm1);
|
||||
t.equal(api2.vm, vm2);
|
||||
|
||||
t.equal(extensions.length, 2);
|
||||
t.equal(extensions[0].length, 1);
|
||||
t.equal(extensions[1].length, 1);
|
||||
t.ok(extensions[0][0] instanceof Extension1);
|
||||
t.ok(extensions[1][0] instanceof Extension2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('register multiple extensions in one script', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
class Extension1 {}
|
||||
class Extension2 {}
|
||||
setScript('https://turbowarp.org/multiple.js', () => {
|
||||
global.Scratch.extensions.register(new Extension1());
|
||||
global.Scratch.extensions.register(new Extension2());
|
||||
});
|
||||
const extensions = await UnsandboxedExtensionRunner.load('https://turbowarp.org/multiple.js', vm);
|
||||
t.equal(extensions.length, 2);
|
||||
t.ok(extensions[0] instanceof Extension1);
|
||||
t.ok(extensions[1] instanceof Extension2);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('extension error results in rejection', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
try {
|
||||
await UnsandboxedExtensionRunner.load('https://turbowarp.org/404.js', vm);
|
||||
// Above should throw an error as the script will not load successfully
|
||||
t.fail();
|
||||
} catch (e) {
|
||||
t.pass();
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ScratchX', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
setScript('https://turbowarp.org/scratchx.js', () => {
|
||||
const ext = {
|
||||
test: () => 2
|
||||
};
|
||||
const descriptor = {
|
||||
blocks: [
|
||||
['r', 'test', 'test']
|
||||
]
|
||||
};
|
||||
global.ScratchExtensions.register('Test', descriptor, ext);
|
||||
});
|
||||
const extensions = await UnsandboxedExtensionRunner.load('https://turbowarp.org/scratchx.js', vm);
|
||||
t.equal(extensions.length, 1);
|
||||
t.equal(extensions[0].test(), 2);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canFetch', async t => {
|
||||
// see tw_security_manager.js
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
const result = global.Scratch.canFetch('https://example.com/');
|
||||
t.type(result, Promise);
|
||||
t.equal(await result, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fetch', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
global.Scratch.canFetch = url => url === 'https://example.com/2';
|
||||
await t.rejects(global.Scratch.fetch('https://example.com/1'), /Permission to fetch https:\/\/example.com\/1 rejected/);
|
||||
await t.rejects(global.Scratch.fetch(new Request('https://example.com/1')), /Permission to fetch https:\/\/example.com\/1 rejected/);
|
||||
t.equal(await global.Scratch.fetch('https://example.com/2'), '[Response https://example.com/2 options={}]');
|
||||
t.equal(await global.Scratch.fetch(new Request('https://example.com/2')), '[Response https://example.com/2 options={}]');
|
||||
t.equal(await global.Scratch.fetch('https://example.com/2', {
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
body: 'abc'
|
||||
}), '[Response https://example.com/2 options={"redirect":"follow","method":"POST","body":"abc"}]');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canOpenWindow', async t => {
|
||||
// see tw_security_manager.js
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
const result = global.Scratch.canOpenWindow('https://example.com/');
|
||||
t.type(result, Promise);
|
||||
t.equal(await result, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('openWindow', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
global.Scratch.canOpenWindow = url => url === 'https://example.com/2';
|
||||
await t.rejects(global.Scratch.openWindow('https://example.com/1'), /Permission to open tab https:\/\/example.com\/1 rejected/);
|
||||
t.equal(await global.Scratch.openWindow('https://example.com/2'), '[Window https://example.com/2 target=_blank features=noreferrer]');
|
||||
t.equal(await global.Scratch.openWindow('https://example.com/2', 'popup=1'), '[Window https://example.com/2 target=_blank features=noreferrer,popup=1]');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canRedirect', async t => {
|
||||
// see tw_security_manager.js
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
const result = global.Scratch.canRedirect('https://example.com/');
|
||||
t.type(result, Promise);
|
||||
t.equal(await result, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('redirect', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
global.Scratch.canRedirect = url => url === 'https://example.com/2';
|
||||
await t.rejects(global.Scratch.redirect('https://example.com/1'), /Permission to redirect to https:\/\/example.com\/1 rejected/);
|
||||
t.equal(global.location.href, 'https://example.com/');
|
||||
await global.Scratch.redirect('https://example.com/2');
|
||||
t.equal(global.location.href, 'https://example.com/2');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('translate', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
t.equal(global.Scratch.translate({
|
||||
id: 'test1',
|
||||
default: 'Message 1: {var}',
|
||||
description: 'Description'
|
||||
}, {
|
||||
var: 'test'
|
||||
}), 'Message 1: test');
|
||||
t.equal(global.Scratch.translate('test1 {var}', {
|
||||
var: 'ok'
|
||||
}), 'test1 ok');
|
||||
t.equal(global.Scratch.translate.language, 'en');
|
||||
|
||||
global.Scratch.translate.setup({
|
||||
en: {
|
||||
test1: 'EN Message 1: {var}'
|
||||
},
|
||||
es: {
|
||||
test1: 'ES Message 1: {var}'
|
||||
}
|
||||
});
|
||||
t.equal(global.Scratch.translate({
|
||||
id: 'test1',
|
||||
default: 'Message 1: {var}',
|
||||
description: 'Description'
|
||||
}, {
|
||||
var: 'test'
|
||||
}), 'EN Message 1: test');
|
||||
t.equal(global.Scratch.translate('test1 {var}', {
|
||||
var: 'ok'
|
||||
}), 'test1 ok');
|
||||
t.equal(global.Scratch.translate.language, 'en');
|
||||
|
||||
await vm.setLocale('es');
|
||||
// do not call setup() again; real extensions will not do that.
|
||||
// need to make sure that the translatiosn are saved after calling setLocale.
|
||||
t.equal(global.Scratch.translate({
|
||||
id: 'test1',
|
||||
default: 'Message 1: {var}',
|
||||
description: 'Description'
|
||||
}, {
|
||||
var: 'test'
|
||||
}), 'ES Message 1: test');
|
||||
t.equal(global.Scratch.translate.language, 'es');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canRecordAudio', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
vm.securityManager.canRecordAudio = () => false;
|
||||
t.equal(await global.Scratch.canRecordAudio(), false);
|
||||
|
||||
vm.securityManager.canRecordAudio = () => true;
|
||||
t.equal(await global.Scratch.canRecordAudio(), true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canRecordVideo', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
vm.securityManager.canRecordVideo = () => false;
|
||||
t.equal(await global.Scratch.canRecordVideo(), false);
|
||||
|
||||
vm.securityManager.canRecordVideo = () => true;
|
||||
t.equal(await global.Scratch.canRecordVideo(), true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canReadClipboard', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
vm.securityManager.canReadClipboard = () => false;
|
||||
t.equal(await global.Scratch.canReadClipboard(), false);
|
||||
|
||||
vm.securityManager.canReadClipboard = () => true;
|
||||
t.equal(await global.Scratch.canReadClipboard(), true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canNotify', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
vm.securityManager.canNotify = () => false;
|
||||
t.equal(await global.Scratch.canNotify(), false);
|
||||
|
||||
vm.securityManager.canNotify = () => true;
|
||||
t.equal(await global.Scratch.canNotify(), true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canGeolocate', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
vm.securityManager.canGeolocate = () => false;
|
||||
t.equal(await global.Scratch.canGeolocate(), false);
|
||||
|
||||
vm.securityManager.canGeolocate = () => true;
|
||||
t.equal(await global.Scratch.canGeolocate(), true);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rewriteExtensionURL', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
let createdRewrittenExtension = false;
|
||||
class RewrittenExtension {
|
||||
getInfo () {
|
||||
createdRewrittenExtension = true;
|
||||
return {
|
||||
id: 'extensionid',
|
||||
blocks: []
|
||||
};
|
||||
}
|
||||
}
|
||||
setScript('https://turbowarp.org/rewritten.js', () => {
|
||||
global.Scratch.extensions.register(new RewrittenExtension());
|
||||
});
|
||||
|
||||
class OriginalExtension {
|
||||
getInfo () {
|
||||
t.fail('Should not create original extension');
|
||||
return {
|
||||
id: 'extensionid',
|
||||
blocks: []
|
||||
};
|
||||
}
|
||||
}
|
||||
setScript('https://turbowarp.org/original.js', () => {
|
||||
global.Scratch.extensions.register(new OriginalExtension());
|
||||
});
|
||||
|
||||
vm.securityManager.getSandboxMode = () => 'unsandboxed';
|
||||
vm.securityManager.rewriteExtensionURL = url => {
|
||||
if (url === 'https://turbowarp.org/original.js') {
|
||||
return 'https://turbowarp.org/rewritten.js';
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
await vm.extensionManager.loadExtensionURL('https://turbowarp.org/original.js');
|
||||
|
||||
t.ok(createdRewrittenExtension, 'used rewritten extension');
|
||||
t.ok(vm.extensionManager.isExtensionURLLoaded('https://turbowarp.org/original.js'), 'marks original URL as loaded');
|
||||
t.notOk(vm.extensionManager.isExtensionURLLoaded('https://turbowarp.org/rewritten.js'), 'does not mark new URL as loaded');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('canEmbed', async t => {
|
||||
const vm = new VirtualMachine();
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
|
||||
vm.securityManager.canEmbed = url => url === 'https://example.com/safe';
|
||||
t.ok(await global.Scratch.canEmbed('https://example.com/safe'));
|
||||
t.notOk(await global.Scratch.canEmbed('https://example.com/unsafe'));
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('CREATE_UNSANDBOXED_EXTENSION_API', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.on('CREATE_UNSANDBOXED_EXTENSION_API', api => {
|
||||
api.extraStuff = 'aaaa';
|
||||
});
|
||||
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
|
||||
t.equal(global.Scratch.extraStuff, 'aaaa');
|
||||
t.end();
|
||||
});
|
||||
64
scratch-vm/test/unit/tw_util_async_limiter.js
Normal file
64
scratch-vm/test/unit/tw_util_async_limiter.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const AsyncLimiter = require('../../src/util/async-limiter');
|
||||
const {test} = require('tap');
|
||||
|
||||
test('Runs callback', async t => {
|
||||
/* eslint-disable-next-line require-await */
|
||||
const callback = async (a, b) => a + b;
|
||||
|
||||
const limiter = new AsyncLimiter(callback, 2);
|
||||
|
||||
t.same(await Promise.all([
|
||||
limiter.do(1, 2),
|
||||
limiter.do(3, 4),
|
||||
limiter.do(5, 6),
|
||||
limiter.do(7, 8),
|
||||
limiter.do(9, 10)
|
||||
]), [
|
||||
3,
|
||||
7,
|
||||
11,
|
||||
15,
|
||||
19
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Errors', async t => {
|
||||
t.plan(1);
|
||||
const errorObject = new Error('Testing testing 123'); // want to get the same *exact* object back
|
||||
const callback = () => Promise.reject(errorObject);
|
||||
const limiter = new AsyncLimiter(callback, 10);
|
||||
try {
|
||||
await limiter.do();
|
||||
} catch (e) {
|
||||
t.equal(e, errorObject);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('Limit and queue', async t => {
|
||||
const calls = [];
|
||||
const callback = () => new Promise(resolve => {
|
||||
calls.push({
|
||||
resolve
|
||||
});
|
||||
});
|
||||
|
||||
const limiter = new AsyncLimiter(callback, 5);
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
limiter.do();
|
||||
}
|
||||
|
||||
t.equal(calls.length, 5);
|
||||
|
||||
calls.forEach(i => i.resolve());
|
||||
await Promise.resolve();
|
||||
t.equal(calls.length, 10);
|
||||
|
||||
calls.forEach(i => i.resolve());
|
||||
await Promise.resolve();
|
||||
t.equal(calls.length, 12);
|
||||
|
||||
t.end();
|
||||
});
|
||||
16
scratch-vm/test/unit/tw_vm_exports.js
Normal file
16
scratch-vm/test/unit/tw_vm_exports.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const {test} = require('tap');
|
||||
const VM = require('../../src/virtual-machine');
|
||||
|
||||
test('exports exist', t => {
|
||||
const vm = new VM();
|
||||
t.type(vm.exports.Sprite, 'function');
|
||||
t.type(vm.exports.RenderedTarget, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('JSZip', t => {
|
||||
const JSZip = new VM().exports.JSZip;
|
||||
const zip = new JSZip();
|
||||
t.type(zip.file, 'function');
|
||||
t.end();
|
||||
});
|
||||
41
scratch-vm/test/unit/util_base64.js
Normal file
41
scratch-vm/test/unit/util_base64.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const test = require('tap').test;
|
||||
const Base64Util = require('../../src/util/base64-util');
|
||||
|
||||
test('uint8ArrayToBase64', t => {
|
||||
t.equal(Base64Util.uint8ArrayToBase64(new Uint8Array([0, 50, 80, 200])), 'ADJQyA==');
|
||||
t.equal(Base64Util.uint8ArrayToBase64([0, 50, 80, 200]), 'ADJQyA==');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('arrayBufferToBase64', t => {
|
||||
t.equal(Base64Util.arrayBufferToBase64(new Uint8Array([0, 50, 80, 200]).buffer), 'ADJQyA==');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('base64ToUint8Array', t => {
|
||||
t.same(Base64Util.base64ToUint8Array('ADJQyA=='), new Uint8Array([0, 50, 80, 200]));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('round trips', t => {
|
||||
const data = [
|
||||
new Uint8Array(new Array(255)
|
||||
.fill()
|
||||
.map((_, index) => index)
|
||||
),
|
||||
new Uint8Array(0),
|
||||
new Uint8Array([10, 90, 0, 255, 255, 255, 10, 2]),
|
||||
new Uint8Array(10000),
|
||||
new Uint8Array(100000)
|
||||
];
|
||||
for (const uint8array of data) {
|
||||
const uint8ToBase64 = Base64Util.uint8ArrayToBase64(uint8array);
|
||||
const arrayToBase64 = Base64Util.uint8ArrayToBase64(Array.from(uint8array));
|
||||
const bufferToBase64 = Base64Util.arrayBufferToBase64(uint8array.buffer);
|
||||
t.equal(uint8ToBase64, arrayToBase64);
|
||||
t.equal(uint8ToBase64, bufferToBase64);
|
||||
const decoded = Base64Util.base64ToUint8Array(uint8ToBase64);
|
||||
t.same(uint8array, decoded);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
201
scratch-vm/test/unit/util_cast.js
Normal file
201
scratch-vm/test/unit/util_cast.js
Normal file
@@ -0,0 +1,201 @@
|
||||
const test = require('tap').test;
|
||||
const cast = require('../../src/util/cast');
|
||||
|
||||
test('toNumber', t => {
|
||||
// Numeric
|
||||
t.strictEqual(cast.toNumber(0), 0);
|
||||
t.strictEqual(cast.toNumber(1), 1);
|
||||
t.strictEqual(cast.toNumber(3.14), 3.14);
|
||||
|
||||
// String
|
||||
t.strictEqual(cast.toNumber('0'), 0);
|
||||
t.strictEqual(cast.toNumber('1'), 1);
|
||||
t.strictEqual(cast.toNumber('3.14'), 3.14);
|
||||
t.strictEqual(cast.toNumber('0.1e10'), 1000000000);
|
||||
t.strictEqual(cast.toNumber('foobar'), 0);
|
||||
|
||||
// Boolean
|
||||
t.strictEqual(cast.toNumber(true), 1);
|
||||
t.strictEqual(cast.toNumber(false), 0);
|
||||
t.strictEqual(cast.toNumber('true'), 0);
|
||||
t.strictEqual(cast.toNumber('false'), 0);
|
||||
|
||||
// Undefined & object
|
||||
t.strictEqual(cast.toNumber(undefined), 0);
|
||||
t.strictEqual(cast.toNumber({}), 0);
|
||||
t.strictEqual(cast.toNumber(NaN), 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('toBoolean', t => {
|
||||
// Numeric
|
||||
t.strictEqual(cast.toBoolean(0), false);
|
||||
t.strictEqual(cast.toBoolean(1), true);
|
||||
t.strictEqual(cast.toBoolean(3.14), true);
|
||||
|
||||
// String
|
||||
t.strictEqual(cast.toBoolean('0'), false);
|
||||
t.strictEqual(cast.toBoolean('1'), true);
|
||||
t.strictEqual(cast.toBoolean('3.14'), true);
|
||||
t.strictEqual(cast.toBoolean('0.1e10'), true);
|
||||
t.strictEqual(cast.toBoolean('foobar'), true);
|
||||
|
||||
// Boolean
|
||||
t.strictEqual(cast.toBoolean(true), true);
|
||||
t.strictEqual(cast.toBoolean(false), false);
|
||||
|
||||
// Undefined & object
|
||||
t.strictEqual(cast.toBoolean(undefined), false);
|
||||
t.strictEqual(cast.toBoolean({}), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('toString', t => {
|
||||
// Numeric
|
||||
t.strictEqual(cast.toString(0), '0');
|
||||
t.strictEqual(cast.toString(1), '1');
|
||||
t.strictEqual(cast.toString(3.14), '3.14');
|
||||
|
||||
// String
|
||||
t.strictEqual(cast.toString('0'), '0');
|
||||
t.strictEqual(cast.toString('1'), '1');
|
||||
t.strictEqual(cast.toString('3.14'), '3.14');
|
||||
t.strictEqual(cast.toString('0.1e10'), '0.1e10');
|
||||
t.strictEqual(cast.toString('foobar'), 'foobar');
|
||||
|
||||
// Boolean
|
||||
t.strictEqual(cast.toString(true), 'true');
|
||||
t.strictEqual(cast.toString(false), 'false');
|
||||
|
||||
// Undefined & object
|
||||
t.strictEqual(cast.toString(undefined), 'undefined');
|
||||
t.strictEqual(cast.toString({}), '[object Object]');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('toRgbColorList', t => {
|
||||
// Hex (minimal, see "color" util tests)
|
||||
t.deepEqual(cast.toRgbColorList('#000'), [0, 0, 0]);
|
||||
t.deepEqual(cast.toRgbColorList('#000000'), [0, 0, 0]);
|
||||
t.deepEqual(cast.toRgbColorList('#fff'), [255, 255, 255]);
|
||||
t.deepEqual(cast.toRgbColorList('#ffffff'), [255, 255, 255]);
|
||||
|
||||
// Decimal (minimal, see "color" util tests)
|
||||
t.deepEqual(cast.toRgbColorList(0), [0, 0, 0]);
|
||||
t.deepEqual(cast.toRgbColorList(1), [0, 0, 1]);
|
||||
t.deepEqual(cast.toRgbColorList(16777215), [255, 255, 255]);
|
||||
|
||||
// Malformed
|
||||
t.deepEqual(cast.toRgbColorList('ffffff'), [0, 0, 0]);
|
||||
t.deepEqual(cast.toRgbColorList('foobar'), [0, 0, 0]);
|
||||
t.deepEqual(cast.toRgbColorList('#nothex'), [0, 0, 0]);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('toRgbColorObject', t => {
|
||||
// Hex (minimal, see "color" util tests)
|
||||
t.deepEqual(cast.toRgbColorObject('#000'), {r: 0, g: 0, b: 0});
|
||||
t.deepEqual(cast.toRgbColorObject('#000000'), {r: 0, g: 0, b: 0});
|
||||
t.deepEqual(cast.toRgbColorObject('#fff'), {r: 255, g: 255, b: 255});
|
||||
t.deepEqual(cast.toRgbColorObject('#ffffff'), {r: 255, g: 255, b: 255});
|
||||
|
||||
// Decimal (minimal, see "color" util tests)
|
||||
t.deepEqual(cast.toRgbColorObject(0), {a: 255, r: 0, g: 0, b: 0});
|
||||
t.deepEqual(cast.toRgbColorObject(1), {a: 255, r: 0, g: 0, b: 1});
|
||||
t.deepEqual(cast.toRgbColorObject(16777215), {a: 255, r: 255, g: 255, b: 255});
|
||||
t.deepEqual(cast.toRgbColorObject('0x80010203'), {a: 128, r: 1, g: 2, b: 3});
|
||||
|
||||
// Malformed
|
||||
t.deepEqual(cast.toRgbColorObject('ffffff'), {a: 255, r: 0, g: 0, b: 0});
|
||||
t.deepEqual(cast.toRgbColorObject('foobar'), {a: 255, r: 0, g: 0, b: 0});
|
||||
t.deepEqual(cast.toRgbColorObject('#nothex'), {a: 255, r: 0, g: 0, b: 0});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('compare', t => {
|
||||
// Numeric
|
||||
t.strictEqual(cast.compare(0, 0), 0);
|
||||
t.strictEqual(cast.compare(1, 0), 1);
|
||||
t.strictEqual(cast.compare(0, 1), -1);
|
||||
t.strictEqual(cast.compare(1, 1), 0);
|
||||
|
||||
// String
|
||||
t.strictEqual(cast.compare('0', '0'), 0);
|
||||
t.strictEqual(cast.compare('0.1e10', '1000000000'), 0);
|
||||
t.strictEqual(cast.compare('foobar', 'FOOBAR'), 0);
|
||||
t.ok(cast.compare('dog', 'cat') > 0);
|
||||
|
||||
// Boolean
|
||||
t.strictEqual(cast.compare(true, true), 0);
|
||||
t.strictEqual(cast.compare(true, false), 1);
|
||||
t.strictEqual(cast.compare(false, true), -1);
|
||||
t.strictEqual(cast.compare(true, true), 0);
|
||||
|
||||
// Undefined & object
|
||||
t.strictEqual(cast.compare(undefined, undefined), 0);
|
||||
t.strictEqual(cast.compare(undefined, 'undefined'), 0);
|
||||
t.strictEqual(cast.compare({}, {}), 0);
|
||||
t.strictEqual(cast.compare({}, '[object Object]'), 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('isInt', t => {
|
||||
// Numeric
|
||||
t.strictEqual(cast.isInt(0), true);
|
||||
t.strictEqual(cast.isInt(1), true);
|
||||
t.strictEqual(cast.isInt(0.0), true);
|
||||
t.strictEqual(cast.isInt(3.14), false);
|
||||
t.strictEqual(cast.isInt(NaN), true);
|
||||
|
||||
// String
|
||||
t.strictEqual(cast.isInt('0'), true);
|
||||
t.strictEqual(cast.isInt('1'), true);
|
||||
t.strictEqual(cast.isInt('0.0'), false);
|
||||
t.strictEqual(cast.isInt('0.1e10'), false);
|
||||
t.strictEqual(cast.isInt('3.14'), false);
|
||||
|
||||
// Boolean
|
||||
t.strictEqual(cast.isInt(true), true);
|
||||
t.strictEqual(cast.isInt(false), true);
|
||||
|
||||
// Undefined & object
|
||||
t.strictEqual(cast.isInt(undefined), false);
|
||||
t.strictEqual(cast.isInt({}), false);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('toListIndex', t => {
|
||||
const list = [0, 1, 2, 3, 4, 5];
|
||||
const empty = [];
|
||||
|
||||
// Valid
|
||||
t.strictEqual(cast.toListIndex(1, list.length, false), 1);
|
||||
t.strictEqual(cast.toListIndex(6, list.length, false), 6);
|
||||
|
||||
// Invalid
|
||||
t.strictEqual(cast.toListIndex(-1, list.length, false), cast.LIST_INVALID);
|
||||
t.strictEqual(cast.toListIndex(0.1, list.length, false), cast.LIST_INVALID);
|
||||
t.strictEqual(cast.toListIndex(0, list.length, false), cast.LIST_INVALID);
|
||||
t.strictEqual(cast.toListIndex(7, list.length, false), cast.LIST_INVALID);
|
||||
|
||||
// "all"
|
||||
t.strictEqual(cast.toListIndex('all', list.length, true), cast.LIST_ALL);
|
||||
t.strictEqual(cast.toListIndex('all', list.length, false), cast.LIST_INVALID);
|
||||
|
||||
// "last"
|
||||
t.strictEqual(cast.toListIndex('last', list.length, false), list.length);
|
||||
t.strictEqual(cast.toListIndex('last', empty.length, false), cast.LIST_INVALID);
|
||||
|
||||
// "random"
|
||||
const random = cast.toListIndex('random', list.length, false);
|
||||
t.ok(random <= list.length);
|
||||
t.ok(random > 0);
|
||||
t.strictEqual(cast.toListIndex('random', empty.length, false), cast.LIST_INVALID);
|
||||
|
||||
// "any" (alias for "random")
|
||||
const any = cast.toListIndex('any', list.length, false);
|
||||
t.ok(any <= list.length);
|
||||
t.ok(any > 0);
|
||||
t.strictEqual(cast.toListIndex('any', empty.length, false), cast.LIST_INVALID);
|
||||
t.end();
|
||||
});
|
||||
135
scratch-vm/test/unit/util_color.js
Normal file
135
scratch-vm/test/unit/util_color.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const test = require('tap').test;
|
||||
const color = require('../../src/util/color');
|
||||
|
||||
/**
|
||||
* Assert that two HSV colors are similar to each other, within a tolerance.
|
||||
* @param {Test} t - the Tap test object.
|
||||
* @param {HSVObject} actual - the first HSV color to compare.
|
||||
* @param {HSVObject} expected - the other HSV color to compare.
|
||||
*/
|
||||
const hsvSimilar = function (t, actual, expected) {
|
||||
if ((Math.abs(actual.h - expected.h) >= 1) ||
|
||||
(Math.abs(actual.s - expected.s) >= 0.01) ||
|
||||
(Math.abs(actual.v - expected.v) >= 0.01)
|
||||
) {
|
||||
t.fail('HSV colors not similar enough', {
|
||||
actual: actual,
|
||||
expected: expected
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that two RGB colors are similar to each other, within a tolerance.
|
||||
* @param {Test} t - the Tap test object.
|
||||
* @param {RGBObject} actual - the first RGB color to compare.
|
||||
* @param {RGBObject} expected - the other RGB color to compare.
|
||||
*/
|
||||
const rgbSimilar = function (t, actual, expected) {
|
||||
if ((Math.abs(actual.r - expected.r) >= 1) ||
|
||||
(Math.abs(actual.g - expected.g) >= 1) ||
|
||||
(Math.abs(actual.b - expected.b) >= 1)
|
||||
) {
|
||||
t.fail('RGB colors not similar enough', {
|
||||
actual: actual,
|
||||
expected: expected
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
test('decimalToHex', t => {
|
||||
t.strictEqual(color.decimalToHex(0), '#000000');
|
||||
t.strictEqual(color.decimalToHex(1), '#000001');
|
||||
t.strictEqual(color.decimalToHex(16777215), '#ffffff');
|
||||
t.strictEqual(color.decimalToHex(-16777215), '#000001');
|
||||
t.strictEqual(color.decimalToHex(99999999), '#5f5e0ff');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('decimalToRgb', t => {
|
||||
t.deepEqual(color.decimalToRgb(0), {a: 255, r: 0, g: 0, b: 0});
|
||||
t.deepEqual(color.decimalToRgb(1), {a: 255, r: 0, g: 0, b: 1});
|
||||
t.deepEqual(color.decimalToRgb(16777215), {a: 255, r: 255, g: 255, b: 255});
|
||||
t.deepEqual(color.decimalToRgb(-16777215), {a: 255, r: 0, g: 0, b: 1});
|
||||
t.deepEqual(color.decimalToRgb(99999999), {a: 5, r: 245, g: 224, b: 255});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('hexToRgb', t => {
|
||||
t.deepEqual(color.hexToRgb('#000'), {r: 0, g: 0, b: 0});
|
||||
t.deepEqual(color.hexToRgb('#000000'), {r: 0, g: 0, b: 0});
|
||||
t.deepEqual(color.hexToRgb('#fff'), {r: 255, g: 255, b: 255});
|
||||
t.deepEqual(color.hexToRgb('#ffffff'), {r: 255, g: 255, b: 255});
|
||||
t.deepEqual(color.hexToRgb('#0fa'), {r: 0, g: 255, b: 170});
|
||||
t.deepEqual(color.hexToRgb('#00ffaa'), {r: 0, g: 255, b: 170});
|
||||
t.deepEqual(color.hexToRgb('#00FFaA'), {r: 0, g: 255, b: 170});
|
||||
|
||||
t.deepEqual(color.hexToRgb('000'), {r: 0, g: 0, b: 0});
|
||||
t.deepEqual(color.hexToRgb('fff'), {r: 255, g: 255, b: 255});
|
||||
t.deepEqual(color.hexToRgb('aBc'), {r: 0xaa, g: 0xbb, b: 0xcc});
|
||||
t.deepEqual(color.hexToRgb('00ffaa'), {r: 0, g: 255, b: 170});
|
||||
|
||||
t.deepEqual(color.hexToRgb('0'), null);
|
||||
t.deepEqual(color.hexToRgb('hello world'), null);
|
||||
t.deepEqual(color.hexToRgb('red'), null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rgbToHex', t => {
|
||||
t.strictEqual(color.rgbToHex({r: 0, g: 0, b: 0}), '#000000');
|
||||
t.strictEqual(color.rgbToHex({r: 255, g: 255, b: 255}), '#ffffff');
|
||||
t.strictEqual(color.rgbToHex({r: 0, g: 255, b: 170}), '#00ffaa');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rgbToDecimal', t => {
|
||||
t.strictEqual(color.rgbToDecimal({r: 0, g: 0, b: 0}), 0);
|
||||
t.strictEqual(color.rgbToDecimal({r: 255, g: 255, b: 255}), 16777215);
|
||||
t.strictEqual(color.rgbToDecimal({r: 0, g: 255, b: 170}), 65450);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('hexToDecimal', t => {
|
||||
t.strictEqual(color.hexToDecimal('#000'), 0);
|
||||
t.strictEqual(color.hexToDecimal('#000000'), 0);
|
||||
t.strictEqual(color.hexToDecimal('#fff'), 16777215);
|
||||
t.strictEqual(color.hexToDecimal('#ffffff'), 16777215);
|
||||
t.strictEqual(color.hexToDecimal('#0fa'), 65450);
|
||||
t.strictEqual(color.hexToDecimal('#00ffaa'), 65450);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('hsvToRgb', t => {
|
||||
rgbSimilar(t, color.hsvToRgb({h: 0, s: 0, v: 0}), {r: 0, g: 0, b: 0});
|
||||
rgbSimilar(t, color.hsvToRgb({h: 123, s: 0.1234, v: 0}), {r: 0, g: 0, b: 0});
|
||||
rgbSimilar(t, color.hsvToRgb({h: 0, s: 0, v: 1}), {r: 255, g: 255, b: 255});
|
||||
rgbSimilar(t, color.hsvToRgb({h: 321, s: 0, v: 1}), {r: 255, g: 255, b: 255});
|
||||
rgbSimilar(t, color.hsvToRgb({h: 0, s: 1, v: 1}), {r: 255, g: 0, b: 0});
|
||||
rgbSimilar(t, color.hsvToRgb({h: 120, s: 1, v: 1}), {r: 0, g: 255, b: 0});
|
||||
rgbSimilar(t, color.hsvToRgb({h: 240, s: 1, v: 1}), {r: 0, g: 0, b: 255});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('rgbToHsv', t => {
|
||||
hsvSimilar(t, color.rgbToHsv({r: 0, g: 0, b: 0}), {h: 0, s: 0, v: 0});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 64, g: 64, b: 64}), {h: 0, s: 0, v: 0.25});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 128, g: 128, b: 128}), {h: 0, s: 0, v: 0.5});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 192, g: 192, b: 192}), {h: 0, s: 0, v: 0.75});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 255, g: 255, b: 255}), {h: 0, s: 0, v: 1});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 255, g: 0, b: 0}), {h: 0, s: 1, v: 1});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 0, g: 255, b: 0}), {h: 120, s: 1, v: 1});
|
||||
hsvSimilar(t, color.rgbToHsv({r: 0, g: 0, b: 255}), {h: 240, s: 1, v: 1});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('mixRgb', t => {
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, -1), {r: 10, g: 20, b: 30});
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0), {r: 10, g: 20, b: 30});
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.25), {r: 15, g: 25, b: 35});
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.5), {r: 20, g: 30, b: 40});
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.75), {r: 25, g: 35, b: 45});
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 1), {r: 30, g: 40, b: 50});
|
||||
rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 2), {r: 30, g: 40, b: 50});
|
||||
t.end();
|
||||
});
|
||||
10
scratch-vm/test/unit/util_jsonrpc-web-socket.js
Normal file
10
scratch-vm/test/unit/util_jsonrpc-web-socket.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const test = require('tap').test;
|
||||
// const JSONRPCWebSocket = require('../../src/util/jsonrpc-web-socket');
|
||||
|
||||
test('constructor', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('dispose', t => {
|
||||
t.end();
|
||||
});
|
||||
18
scratch-vm/test/unit/util_jsonrpc.js
Normal file
18
scratch-vm/test/unit/util_jsonrpc.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const test = require('tap').test;
|
||||
// const JSONRPC = require('../../src/util/jsonrpc');
|
||||
|
||||
test('constructor', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('sendRemoteRequest', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('sendRemoteNotification', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('didReceiveCall', t => {
|
||||
t.end();
|
||||
});
|
||||
70
scratch-vm/test/unit/util_math.js
Normal file
70
scratch-vm/test/unit/util_math.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const test = require('tap').test;
|
||||
const math = require('../../src/util/math-util');
|
||||
|
||||
test('degToRad', t => {
|
||||
t.strictEqual(math.degToRad(0), 0);
|
||||
t.strictEqual(math.degToRad(1), 0.017453292519943295);
|
||||
t.strictEqual(math.degToRad(180), Math.PI);
|
||||
t.strictEqual(math.degToRad(360), 2 * Math.PI);
|
||||
t.strictEqual(math.degToRad(720), 4 * Math.PI);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('radToDeg', t => {
|
||||
t.strictEqual(math.radToDeg(0), 0);
|
||||
t.strictEqual(math.radToDeg(1), 57.29577951308232);
|
||||
t.strictEqual(math.radToDeg(180), 10313.240312354817);
|
||||
t.strictEqual(math.radToDeg(360), 20626.480624709635);
|
||||
t.strictEqual(math.radToDeg(720), 41252.96124941927);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('clamp', t => {
|
||||
t.strictEqual(math.clamp(0, 0, 10), 0);
|
||||
t.strictEqual(math.clamp(1, 0, 10), 1);
|
||||
t.strictEqual(math.clamp(-10, 0, 10), 0);
|
||||
t.strictEqual(math.clamp(100, 0, 10), 10);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('wrapClamp', t => {
|
||||
t.strictEqual(math.wrapClamp(0, 0, 10), 0);
|
||||
t.strictEqual(math.wrapClamp(1, 0, 10), 1);
|
||||
t.strictEqual(math.wrapClamp(-10, 0, 10), 1);
|
||||
t.strictEqual(math.wrapClamp(100, 0, 10), 1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('tan', t => {
|
||||
t.strictEqual(math.tan(90), Infinity);
|
||||
t.strictEqual(math.tan(180), 0);
|
||||
t.strictEqual(math.tan(-90), -Infinity);
|
||||
t.strictEqual(math.tan(33), 0.6494075932);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('reducedSortOrdering', t => {
|
||||
t.deepEqual(math.reducedSortOrdering([5, 18, 6, 3]), [1, 3, 2, 0]);
|
||||
t.deepEqual(math.reducedSortOrdering([5, 1, 56, 19]), [1, 0, 3, 2]);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('inclusiveRandIntWithout', t => {
|
||||
const withRandomValue = function (randValue, ...args) {
|
||||
const oldMathRandom = Math.random;
|
||||
Object.assign(global.Math, {random: () => randValue});
|
||||
const result = math.inclusiveRandIntWithout(...args);
|
||||
Object.assign(global.Math, {random: oldMathRandom});
|
||||
return result;
|
||||
};
|
||||
|
||||
t.strictEqual(withRandomValue(3 / 6, 0, 6, 2), 4);
|
||||
t.strictEqual(withRandomValue(2 / 6, 0, 6, 2), 3);
|
||||
t.strictEqual(withRandomValue(1 / 6, 0, 6, 2), 1);
|
||||
t.strictEqual(withRandomValue(1.9 / 6, 0, 6, 2), 1);
|
||||
|
||||
t.strictEqual(withRandomValue(3 / 4, 10, 14, 10), 14);
|
||||
t.strictEqual(withRandomValue(0 / 4, 10, 14, 10), 11);
|
||||
|
||||
t.end();
|
||||
});
|
||||
63
scratch-vm/test/unit/util_new-block-ids.js
Normal file
63
scratch-vm/test/unit/util_new-block-ids.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const newBlockIds = require('../../src/util/new-block-ids');
|
||||
const simpleStack = require('../fixtures/simple-stack');
|
||||
const tap = require('tap');
|
||||
const test = tap.test;
|
||||
|
||||
let originals;
|
||||
let newBlocks;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
originals = simpleStack;
|
||||
// Will be mutated so make a copy first
|
||||
newBlocks = JSON.parse(JSON.stringify(simpleStack));
|
||||
newBlockIds(newBlocks);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The structure of the simple stack is:
|
||||
* moveTo (looks_size) -> stopAllSounds
|
||||
* The list of blocks is
|
||||
* 0: moveTo (TO input block: 1, shadow: 2)
|
||||
* 1: looks_size (parent: 0)
|
||||
* 2: obscured shadow for moveTo input (parent: 0)
|
||||
* 3: stopAllSounds (parent: 0)
|
||||
* Inspect fixtures/simple-stack for the full object.
|
||||
*/
|
||||
|
||||
test('top-level block IDs have all changed', t => {
|
||||
newBlocks.forEach((block, i) => {
|
||||
t.notEqual(block.id, originals[i].id);
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('input reference is maintained on parent for attached block', t => {
|
||||
t.equal(newBlocks[0].inputs.TO.block, newBlocks[1].id);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('input reference is maintained on parent for obscured shadow', t => {
|
||||
t.equal(newBlocks[0].inputs.TO.shadow, newBlocks[2].id);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('parent reference is maintained for attached input', t => {
|
||||
t.equal(newBlocks[1].parent, newBlocks[0].id);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('parent reference is maintained for obscured shadow', t => {
|
||||
t.equal(newBlocks[2].parent, newBlocks[0].id);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('parent reference is maintained for next block', t => {
|
||||
t.equal(newBlocks[3].parent, newBlocks[0].id);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('next reference is maintained for previous block', t => {
|
||||
t.equal(newBlocks[0].next, newBlocks[3].id);
|
||||
t.end();
|
||||
});
|
||||
32
scratch-vm/test/unit/util_rateLimiter.js
Normal file
32
scratch-vm/test/unit/util_rateLimiter.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const test = require('tap').test;
|
||||
const RateLimiter = require('../../src/util/rateLimiter.js');
|
||||
|
||||
test('rate limiter', t => {
|
||||
// Create a rate limiter with maximum of 20 sends per second
|
||||
const rate = 20;
|
||||
const limiter = new RateLimiter(rate);
|
||||
|
||||
// Simulate time passing with a stubbed timer
|
||||
let simulatedTime = Date.now();
|
||||
limiter._timer = {timeElapsed: () => simulatedTime};
|
||||
|
||||
// The rate limiter starts with a number of tokens equal to the max rate
|
||||
t.equal(limiter._count, rate);
|
||||
|
||||
// Running okayToSend a number of times equal to the max rate
|
||||
// uses up all of the tokens
|
||||
for (let i = 0; i < rate; i++) {
|
||||
t.true(limiter.okayToSend());
|
||||
// Tokens are counting down
|
||||
t.equal(limiter._count, rate - (i + 1));
|
||||
}
|
||||
t.false(limiter.okayToSend());
|
||||
|
||||
// Advance the timer enough so we get exactly one more token
|
||||
// One extra millisecond is required to get over the threshold
|
||||
simulatedTime += (1000 / rate) + 1;
|
||||
t.true(limiter.okayToSend());
|
||||
t.false(limiter.okayToSend());
|
||||
|
||||
t.end();
|
||||
});
|
||||
129
scratch-vm/test/unit/util_string.js
Normal file
129
scratch-vm/test/unit/util_string.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const test = require('tap').test;
|
||||
const StringUtil = require('../../src/util/string-util');
|
||||
|
||||
test('splitFirst', t => {
|
||||
t.deepEqual(StringUtil.splitFirst('asdf.1234', '.'), ['asdf', '1234']);
|
||||
t.deepEqual(StringUtil.splitFirst('asdf.', '.'), ['asdf', '']);
|
||||
t.deepEqual(StringUtil.splitFirst('.1234', '.'), ['', '1234']);
|
||||
t.deepEqual(StringUtil.splitFirst('foo', '.'), ['foo', null]);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('withoutTrailingDigits', t => {
|
||||
t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing');
|
||||
t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 ');
|
||||
t.strictEqual(StringUtil.withoutTrailingDigits('boeing𝟨'), 'boeing𝟨');
|
||||
t.strictEqual(StringUtil.withoutTrailingDigits('boeing 747'), 'boeing ');
|
||||
t.strictEqual(StringUtil.withoutTrailingDigits('747'), '');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('unusedName', t => {
|
||||
t.strictEqual(
|
||||
StringUtil.unusedName(
|
||||
'name',
|
||||
['not the same name']
|
||||
),
|
||||
'name'
|
||||
);
|
||||
t.strictEqual(
|
||||
StringUtil.unusedName(
|
||||
'name',
|
||||
['name']
|
||||
),
|
||||
'name2'
|
||||
);
|
||||
t.strictEqual(
|
||||
StringUtil.unusedName(
|
||||
'name',
|
||||
['name30']
|
||||
),
|
||||
'name'
|
||||
);
|
||||
t.strictEqual(
|
||||
StringUtil.unusedName(
|
||||
'name',
|
||||
['name', 'name2']
|
||||
),
|
||||
'name3'
|
||||
);
|
||||
t.strictEqual(
|
||||
StringUtil.unusedName(
|
||||
'name',
|
||||
['name', 'name3']
|
||||
),
|
||||
'name2'
|
||||
);
|
||||
t.strictEqual(
|
||||
StringUtil.unusedName(
|
||||
'boeing747',
|
||||
['boeing747']
|
||||
),
|
||||
'boeing2' // Yup, this matches scratch-flash...
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('stringify', t => {
|
||||
const obj = {
|
||||
a: Infinity,
|
||||
b: NaN,
|
||||
c: -Infinity,
|
||||
d: 23,
|
||||
e: 'str',
|
||||
f: {
|
||||
nested: Infinity
|
||||
}
|
||||
};
|
||||
const parsed = JSON.parse(StringUtil.stringify(obj));
|
||||
t.equal(parsed.a, 0);
|
||||
t.equal(parsed.b, 0);
|
||||
t.equal(parsed.c, 0);
|
||||
t.equal(parsed.d, 23);
|
||||
t.equal(parsed.e, 'str');
|
||||
t.equal(parsed.f.nested, 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('replaceUnsafeChars', t => {
|
||||
const empty = '';
|
||||
t.equal(StringUtil.replaceUnsafeChars(empty), empty);
|
||||
|
||||
const safe = 'hello';
|
||||
t.equal(StringUtil.replaceUnsafeChars(safe), safe);
|
||||
|
||||
const unsafe = '< > & \' "';
|
||||
t.equal(StringUtil.replaceUnsafeChars(unsafe), 'lt gt amp apos quot');
|
||||
|
||||
const single = '&';
|
||||
t.equal(StringUtil.replaceUnsafeChars(single), 'amp');
|
||||
|
||||
const mix = '<a>b& c\'def_-"';
|
||||
t.equal(StringUtil.replaceUnsafeChars(mix), 'ltagtbamp caposdef_-quot');
|
||||
|
||||
const dupes = '<<&_"_"_&>>';
|
||||
t.equal(StringUtil.replaceUnsafeChars(dupes), 'ltltamp_quot_quot_ampgtgt');
|
||||
|
||||
const emoji = '(>^_^)>';
|
||||
t.equal(StringUtil.replaceUnsafeChars(emoji), '(gt^_^)gt');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('replaceUnsafeChars should handle non strings', t => {
|
||||
const array = ['hello', 'world'];
|
||||
t.equal(StringUtil.replaceUnsafeChars(array), String(array));
|
||||
|
||||
const arrayWithSpecialChar = ['hello', '<world>'];
|
||||
t.equal(StringUtil.replaceUnsafeChars(arrayWithSpecialChar), 'hello,ltworldgt');
|
||||
|
||||
const arrayWithNumbers = [1, 2, 3];
|
||||
t.equal(StringUtil.replaceUnsafeChars(arrayWithNumbers), '1,2,3');
|
||||
|
||||
// Objects shouldn't get provided to replaceUnsafeChars, but in the event
|
||||
// they do, it should just return the object (and log an error)
|
||||
const object = {hello: 'world'};
|
||||
t.equal(StringUtil.replaceUnsafeChars(object), object);
|
||||
|
||||
t.end();
|
||||
});
|
||||
192
scratch-vm/test/unit/util_task-queue.js
Normal file
192
scratch-vm/test/unit/util_task-queue.js
Normal file
@@ -0,0 +1,192 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const TaskQueue = require('../../src/util/task-queue');
|
||||
|
||||
const MockTimer = require('../fixtures/mock-timer');
|
||||
const testCompare = require('../fixtures/test-compare');
|
||||
|
||||
// Max tokens = 1000
|
||||
// Refill 1000 tokens per second (1 per millisecond)
|
||||
// Token bucket starts empty
|
||||
// Max total cost of queued tasks = 10000 tokens = 10 seconds
|
||||
const makeTestQueue = () => {
|
||||
const bukkit = new TaskQueue(1000, 1000, {
|
||||
startingTokens: 0,
|
||||
maxTotalCost: 10000
|
||||
});
|
||||
|
||||
const mockTimer = new MockTimer();
|
||||
bukkit._timer = mockTimer;
|
||||
mockTimer.start();
|
||||
|
||||
return bukkit;
|
||||
};
|
||||
|
||||
test('spec', t => {
|
||||
t.type(TaskQueue, 'function');
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
t.type(bukkit, 'object');
|
||||
|
||||
t.type(bukkit.length, 'number');
|
||||
t.type(bukkit.do, 'function');
|
||||
t.type(bukkit.cancel, 'function');
|
||||
t.type(bukkit.cancelAll, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('constructor', t => {
|
||||
t.ok(new TaskQueue(1, 1));
|
||||
t.ok(new TaskQueue(1, 1, {}));
|
||||
t.ok(new TaskQueue(1, 1, {startingTokens: 0}));
|
||||
t.ok(new TaskQueue(1, 1, {maxTotalCost: 999}));
|
||||
t.ok(new TaskQueue(1, 1, {startingTokens: 0, maxTotalCost: 999}));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('run tasks', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
const taskResults = [];
|
||||
|
||||
const promises = [
|
||||
bukkit.do(() => {
|
||||
taskResults.push('a');
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '>=', 50, 'Costly task must wait');
|
||||
}, 50),
|
||||
bukkit.do(() => {
|
||||
taskResults.push('b');
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '>=', 60, 'Tasks must run in serial');
|
||||
}, 10),
|
||||
bukkit.do(() => {
|
||||
taskResults.push('c');
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '<=', 70, 'Cheap task should run soon');
|
||||
}, 1)
|
||||
];
|
||||
|
||||
// advance 10 simulated milliseconds per JS tick
|
||||
while (bukkit.length > 0) {
|
||||
await bukkit._timer.advanceMockTimeAsync(10);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
t.deepEqual(taskResults, ['a', 'b', 'c'], 'All tasks must run in correct order');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('cancel', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
const taskResults = [];
|
||||
const goodCancelMessage = 'Task was canceled correctly';
|
||||
const afterCancelMessage = 'Task was run correctly';
|
||||
const cancelTaskPromise = bukkit.do(
|
||||
() => {
|
||||
taskResults.push('nope');
|
||||
}, 999);
|
||||
const cancelCheckPromise = cancelTaskPromise.then(
|
||||
() => {
|
||||
t.fail('Task should have been canceled');
|
||||
},
|
||||
() => {
|
||||
taskResults.push(goodCancelMessage);
|
||||
}
|
||||
);
|
||||
const keepTaskPromise = bukkit.do(
|
||||
() => {
|
||||
taskResults.push(afterCancelMessage);
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '<', 10, 'Canceled task must not delay other tasks');
|
||||
}, 5);
|
||||
|
||||
// give the bucket a chance to make a mistake
|
||||
await bukkit._timer.advanceMockTimeAsync(1);
|
||||
|
||||
t.equal(bukkit.length, 2);
|
||||
const taskWasCanceled = bukkit.cancel(cancelTaskPromise);
|
||||
t.ok(taskWasCanceled);
|
||||
t.equal(bukkit.length, 1);
|
||||
|
||||
while (bukkit.length > 0) {
|
||||
await bukkit._timer.advanceMockTimeAsync(1);
|
||||
}
|
||||
|
||||
return Promise.all([cancelCheckPromise, keepTaskPromise]).then(() => {
|
||||
t.deepEqual(taskResults, [goodCancelMessage, afterCancelMessage]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('cancelAll', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
const taskResults = [];
|
||||
const goodCancelMessage1 = 'Task1 was canceled correctly';
|
||||
const goodCancelMessage2 = 'Task2 was canceled correctly';
|
||||
|
||||
const promises = [
|
||||
bukkit.do(() => taskResults.push('nope'), 999).then(
|
||||
() => {
|
||||
t.fail('Task1 should have been canceled');
|
||||
},
|
||||
() => {
|
||||
taskResults.push(goodCancelMessage1);
|
||||
}
|
||||
),
|
||||
bukkit.do(() => taskResults.push('nah'), 999).then(
|
||||
() => {
|
||||
t.fail('Task2 should have been canceled');
|
||||
},
|
||||
() => {
|
||||
taskResults.push(goodCancelMessage2);
|
||||
}
|
||||
)
|
||||
];
|
||||
|
||||
// advance time, but not enough that any task should run
|
||||
await bukkit._timer.advanceMockTimeAsync(100);
|
||||
|
||||
bukkit.cancelAll();
|
||||
|
||||
// advance enough that both tasks would run if they hadn't been canceled
|
||||
await bukkit._timer.advanceMockTimeAsync(10000);
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
t.deepEqual(taskResults, [goodCancelMessage1, goodCancelMessage2], 'Tasks should cancel in order');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('max total cost', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
let numTasks = 0;
|
||||
|
||||
const task = () => ++numTasks;
|
||||
|
||||
// Fill the queue
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
bukkit.do(task, 1000);
|
||||
}
|
||||
|
||||
// This one should be rejected because the queue is full
|
||||
bukkit
|
||||
.do(task, 1000)
|
||||
.then(
|
||||
() => {
|
||||
t.fail('Full queue did not reject task');
|
||||
},
|
||||
() => {
|
||||
t.pass();
|
||||
}
|
||||
);
|
||||
|
||||
while (bukkit.length > 0) {
|
||||
await bukkit._timer.advanceMockTimeAsync(1000);
|
||||
}
|
||||
|
||||
// this should be 10 if the last task is rejected or 11 if it runs
|
||||
t.equal(numTasks, 10);
|
||||
t.end();
|
||||
});
|
||||
64
scratch-vm/test/unit/util_timer.js
Normal file
64
scratch-vm/test/unit/util_timer.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const test = require('tap').test;
|
||||
const Timer = require('../../src/util/timer');
|
||||
|
||||
// Stubbed current time
|
||||
let NOW = 0;
|
||||
|
||||
const testNow = {
|
||||
now: () => {
|
||||
NOW += 100;
|
||||
return NOW;
|
||||
}
|
||||
};
|
||||
|
||||
test('spec', t => {
|
||||
const timer = new Timer(testNow);
|
||||
|
||||
t.type(Timer, 'function');
|
||||
t.type(timer, 'object');
|
||||
|
||||
t.type(timer.startTime, 'number');
|
||||
t.type(timer.time, 'function');
|
||||
t.type(timer.start, 'function');
|
||||
t.type(timer.timeElapsed, 'function');
|
||||
t.type(timer.setTimeout, 'function');
|
||||
t.type(timer.clearTimeout, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('time', t => {
|
||||
const timer = new Timer(testNow);
|
||||
const time = timer.time();
|
||||
|
||||
t.ok(testNow.now() >= time);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('start / timeElapsed', t => {
|
||||
const timer = new Timer(testNow);
|
||||
const delay = 100;
|
||||
const threshold = 1000 / 60; // 60 hz
|
||||
|
||||
// Start timer
|
||||
timer.start();
|
||||
|
||||
// Measure timer
|
||||
const timeElapsed = timer.timeElapsed();
|
||||
t.ok(timeElapsed >= 0);
|
||||
t.ok(timeElapsed >= (delay - threshold) &&
|
||||
timeElapsed <= (delay + threshold));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setTimeout / clearTimeout', t => new Promise((resolve, reject) => {
|
||||
const timer = new Timer(testNow);
|
||||
const cancelId = timer.setTimeout(() => {
|
||||
reject(new Error('Canceled task ran'));
|
||||
}, 1);
|
||||
timer.setTimeout(() => {
|
||||
resolve('Non-canceled task ran');
|
||||
t.end();
|
||||
}, 2);
|
||||
timer.clearTimeout(cancelId);
|
||||
}));
|
||||
83
scratch-vm/test/unit/util_variable.js
Normal file
83
scratch-vm/test/unit/util_variable.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const tap = require('tap');
|
||||
const Target = require('../../src/engine/target');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const VariableUtil = require('../../src/util/variable-util');
|
||||
|
||||
let target1;
|
||||
let target2;
|
||||
|
||||
tap.beforeEach(() => {
|
||||
const runtime = new Runtime();
|
||||
target1 = new Target(runtime);
|
||||
target1.blocks.createBlock({
|
||||
id: 'a block',
|
||||
fields: {
|
||||
VARIABLE: {
|
||||
id: 'id1',
|
||||
value: 'foo'
|
||||
}
|
||||
}
|
||||
});
|
||||
target1.blocks.createBlock({
|
||||
id: 'another block',
|
||||
fields: {
|
||||
TEXT: {
|
||||
value: 'not a variable'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
target2 = new Target(runtime);
|
||||
target2.blocks.createBlock({
|
||||
id: 'a different block',
|
||||
fields: {
|
||||
VARIABLE: {
|
||||
id: 'id2',
|
||||
value: 'bar'
|
||||
}
|
||||
}
|
||||
});
|
||||
target2.blocks.createBlock({
|
||||
id: 'another var block',
|
||||
fields: {
|
||||
VARIABLE: {
|
||||
id: 'id1',
|
||||
value: 'foo'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const test = tap.test;
|
||||
|
||||
test('get all var refs', t => {
|
||||
const allVarRefs = VariableUtil.getAllVarRefsForTargets([target1, target2]);
|
||||
t.equal(Object.keys(allVarRefs).length, 2);
|
||||
t.equal(allVarRefs.id1.length, 2);
|
||||
t.equal(allVarRefs.id2.length, 1);
|
||||
t.equal(allVarRefs['not a variable'], undefined);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('merge variable ids', t => {
|
||||
// Redo the id for the variable with 'id1'
|
||||
VariableUtil.updateVariableIdentifiers(target1.blocks.getAllVariableAndListReferences().id1, 'renamed id');
|
||||
const varField = target1.blocks.getBlock('a block').fields.VARIABLE;
|
||||
t.equals(varField.id, 'renamed id');
|
||||
t.equals(varField.value, 'foo');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('merge variable ids but with new name too', t => {
|
||||
// Redo the id for the variable with 'id1'
|
||||
VariableUtil.updateVariableIdentifiers(target1.blocks.getAllVariableAndListReferences().id1, 'renamed id', 'baz');
|
||||
const varField = target1.blocks.getBlock('a block').fields.VARIABLE;
|
||||
t.equals(varField.id, 'renamed id');
|
||||
t.equals(varField.value, 'baz');
|
||||
|
||||
t.end();
|
||||
});
|
||||
52
scratch-vm/test/unit/util_xml.js
Normal file
52
scratch-vm/test/unit/util_xml.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const test = require('tap').test;
|
||||
const xml = require('../../src/util/xml-escape');
|
||||
|
||||
test('escape', t => {
|
||||
const input = '<foo bar="he & llo \'"></foo>';
|
||||
const output = '<foo bar="he & llo '"></foo>';
|
||||
t.strictEqual(xml(input), output);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('xmlEscape (more)', t => {
|
||||
const empty = '';
|
||||
t.equal(xml(empty), empty);
|
||||
|
||||
const safe = 'hello';
|
||||
t.equal(xml(safe), safe);
|
||||
|
||||
const unsafe = '< > & \' "';
|
||||
t.equal(xml(unsafe), '< > & ' "');
|
||||
|
||||
const single = '&';
|
||||
t.equal(xml(single), '&');
|
||||
|
||||
const mix = '<a>b& c\'def_-"';
|
||||
t.equal(xml(mix), '<a>b& c'def_-"');
|
||||
|
||||
const dupes = '<<&_"_"_&>>';
|
||||
t.equal(xml(dupes), '<<&_"_"_&>>');
|
||||
|
||||
const emoji = '(>^_^)>';
|
||||
t.equal(xml(emoji), '(>^_^)>');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('xmlEscape should handle non strings', t => {
|
||||
const array = ['hello', 'world'];
|
||||
t.equal(xml(array), String(array));
|
||||
|
||||
const arrayWithSpecialChar = ['hello', '<world>'];
|
||||
t.equal(xml(arrayWithSpecialChar), 'hello,<world>');
|
||||
|
||||
const arrayWithNumbers = [1, 2, 3];
|
||||
t.equal(xml(arrayWithNumbers), '1,2,3');
|
||||
|
||||
// Objects shouldn't get provided to replaceUnsafeChars, but in the event
|
||||
// they do, it should just return the object (and log an error)
|
||||
const object = {hello: 'world'};
|
||||
t.equal(xml(object), object);
|
||||
|
||||
t.end();
|
||||
});
|
||||
1072
scratch-vm/test/unit/virtual-machine.js
Normal file
1072
scratch-vm/test/unit/virtual-machine.js
Normal file
File diff suppressed because it is too large
Load Diff
93
scratch-vm/test/unit/virtual-machine_tw.js
Normal file
93
scratch-vm/test/unit/virtual-machine_tw.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const test = require('tap').test;
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
|
||||
test('emitTargetsUpdate targetList is lazy', t => {
|
||||
const vm = new VirtualMachine();
|
||||
let calledToJSON = false;
|
||||
vm.runtime.targets = [{
|
||||
toJSON () {
|
||||
calledToJSON = true;
|
||||
return {};
|
||||
}
|
||||
}];
|
||||
let targetsUpdateEvent;
|
||||
vm.on('targetsUpdate', e => {
|
||||
targetsUpdateEvent = e;
|
||||
});
|
||||
vm.emitTargetsUpdate();
|
||||
t.equal(calledToJSON, false);
|
||||
void targetsUpdateEvent.targetList; // should trigger lazy compute
|
||||
t.equal(calledToJSON, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('non-primitive values in lists and variables converted to strings', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const sprite = new Sprite();
|
||||
const target = new RenderedTarget(sprite, vm.runtime);
|
||||
|
||||
target.variables.var1 = new Variable('var', 'test var', Variable.SCALAR_TYPE, false);
|
||||
target.variables.var1.value = null;
|
||||
|
||||
target.variables.var2 = new Variable('var2', 'test var', Variable.SCALAR_TYPE, false);
|
||||
target.variables.var2.value = undefined;
|
||||
|
||||
target.variables.var3 = new Variable('var3', 'test var', Variable.SCALAR_TYPE, false);
|
||||
target.variables.var3.value = {};
|
||||
|
||||
target.variables.var4 = new Variable('var4', 'test var', Variable.SCALAR_TYPE, false);
|
||||
target.variables.var4.value = 1;
|
||||
|
||||
target.variables.var5 = new Variable('var5', 'test var', Variable.SCALAR_TYPE, false);
|
||||
target.variables.var5.value = 'abc';
|
||||
|
||||
target.variables.var6 = new Variable('var6', 'test var', Variable.SCALAR_TYPE, false);
|
||||
target.variables.var6.value = false;
|
||||
|
||||
target.variables.list = new Variable('list', 'test list', Variable.LIST_TYPE, false);
|
||||
target.variables.list.value = ['abc', false, 1, null, undefined, {}];
|
||||
|
||||
vm.runtime.addTarget(target);
|
||||
|
||||
const json = JSON.parse(vm.toJSON());
|
||||
|
||||
t.deepEqual(json.targets[0].variables.var1[1], 'null');
|
||||
t.deepEqual(json.targets[0].variables.var2[1], 'undefined');
|
||||
t.deepEqual(json.targets[0].variables.var3[1], '[object Object]');
|
||||
t.deepEqual(json.targets[0].variables.var4[1], 1);
|
||||
t.deepEqual(json.targets[0].variables.var5[1], 'abc');
|
||||
t.deepEqual(json.targets[0].variables.var6[1], false);
|
||||
|
||||
t.deepEqual(json.targets[0].lists.list[1], ['abc', false, 1, 'null', 'undefined', '[object Object]']);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('addSound error handling when sprite does not exist', async t => {
|
||||
t.plan(1);
|
||||
const vm = new VirtualMachine();
|
||||
const id = 'Inva1id5pri731D$!';
|
||||
try {
|
||||
await vm.addSound({
|
||||
thisObjectDoesNotMatter: true
|
||||
}, id);
|
||||
} catch (e) {
|
||||
if (e && e.message === `No target with ID: ${id}`) {
|
||||
t.pass();
|
||||
}
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('convertToPackagedRuntime forwards to runtime', t => {
|
||||
t.plan(1);
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.convertToPackagedRuntime = () => {
|
||||
t.pass();
|
||||
};
|
||||
vm.convertToPackagedRuntime();
|
||||
t.end();
|
||||
});
|
||||
24
scratch-vm/test/unit/vm_collectAssets.js
Normal file
24
scratch-vm/test/unit/vm_collectAssets.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const RenderedTarget = require('../../src/sprites/rendered-target');
|
||||
const Sprite = require('../../src/sprites/sprite');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
test('collectAssets', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const sprite = new Sprite(null, vm.runtime);
|
||||
const target = new RenderedTarget(sprite, vm.runtime);
|
||||
vm.runtime.targets = [target];
|
||||
const [
|
||||
soundAsset1,
|
||||
soundAsset2,
|
||||
costumeAsset1
|
||||
] = [{assetId: 1}, {assetId: 2}, {assetId: 3}];
|
||||
sprite.sounds = [{id: 1, asset: soundAsset1}, {id: 2, asset: soundAsset2}];
|
||||
sprite.costumes = [{id: 1, asset: costumeAsset1}];
|
||||
const assets = vm.assets;
|
||||
t.type(assets.length, 'number');
|
||||
t.equal(assets.length, 3);
|
||||
t.deepEqual(assets, [soundAsset1, soundAsset2, costumeAsset1]);
|
||||
t.end();
|
||||
});
|
||||
Reference in New Issue
Block a user