Spaces:
Running
Running
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(); | |
}); | |