Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
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();
|
|
});
|