File size: 11,428 Bytes
30c32c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
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();
});