mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-06 03:00:30 -04:00
Add tests for load costume error handling. Fix issue where asset data was not getting saved out properly.
This commit is contained in:
parent
a8618b378f
commit
70a78cf7db
15 changed files with 418 additions and 13 deletions
|
@ -67,6 +67,7 @@
|
||||||
"gh-pages": "1.2.0",
|
"gh-pages": "1.2.0",
|
||||||
"in-publish": "2.0.1",
|
"in-publish": "2.0.1",
|
||||||
"jsdoc": "3.6.6",
|
"jsdoc": "3.6.6",
|
||||||
|
"js-md5": "0.7.3",
|
||||||
"json": "^9.0.4",
|
"json": "^9.0.4",
|
||||||
"lodash.defaultsdeep": "4.6.1",
|
"lodash.defaultsdeep": "4.6.1",
|
||||||
"pngjs": "3.3.3",
|
"pngjs": "3.3.3",
|
||||||
|
|
|
@ -251,8 +251,8 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) {
|
||||||
// and preserve as much of the original costume data as possible
|
// and preserve as much of the original costume data as possible
|
||||||
// Returns a promise of a costume
|
// Returns a promise of a costume
|
||||||
const handleCostumeLoadError = function (costume, runtime) {
|
const handleCostumeLoadError = function (costume, runtime) {
|
||||||
// Keep track of the old assetId until we're done loading the default costume
|
// Keep track of the old asset information until we're done loading the default costume
|
||||||
// const oldAsset = costume.asset; // could be null
|
const oldAsset = costume.asset; // could be null
|
||||||
const oldAssetId = costume.assetId;
|
const oldAssetId = costume.assetId;
|
||||||
const oldRotationX = costume.rotationCenterX;
|
const oldRotationX = costume.rotationCenterX;
|
||||||
const oldRotationY = costume.rotationCenterY;
|
const oldRotationY = costume.rotationCenterY;
|
||||||
|
@ -270,8 +270,10 @@ const handleCostumeLoadError = function (costume, runtime) {
|
||||||
loadedCostume.broken = {};
|
loadedCostume.broken = {};
|
||||||
loadedCostume.broken.assetId = oldAssetId;
|
loadedCostume.broken.assetId = oldAssetId;
|
||||||
loadedCostume.broken.md5 = `${oldAssetId}.${costume.dataFormat}`;
|
loadedCostume.broken.md5 = `${oldAssetId}.${costume.dataFormat}`;
|
||||||
|
|
||||||
// Should be null if we got here because the costume was missing
|
// Should be null if we got here because the costume was missing
|
||||||
loadedCostume.broken.asset = runtime.storage.get(oldAssetId);
|
loadedCostume.broken.asset = oldAsset;
|
||||||
|
|
||||||
loadedCostume.broken.rotationCenterX = oldRotationX;
|
loadedCostume.broken.rotationCenterX = oldRotationX;
|
||||||
loadedCostume.broken.rotationCenterY = oldRotationY;
|
loadedCostume.broken.rotationCenterY = oldRotationY;
|
||||||
return loadedCostume;
|
return loadedCostume;
|
||||||
|
@ -310,7 +312,7 @@ const loadCostumeFromAsset = function (costume, runtime, optVersion) {
|
||||||
if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) {
|
if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) {
|
||||||
return loadVector_(costume, runtime, rotationCenter, optVersion)
|
return loadVector_(costume, runtime, rotationCenter, optVersion)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
log.warn(`Error loading vector image: ${error.name}: ${error.message}`);
|
log.warn(`Error loading vector image: ${error}`);
|
||||||
return handleCostumeLoadError(costume, runtime);
|
return handleCostumeLoadError(costume, runtime);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -427,11 +427,9 @@ class VirtualMachine extends EventEmitter {
|
||||||
* specified by optZipType or blob by default.
|
* specified by optZipType or blob by default.
|
||||||
*/
|
*/
|
||||||
exportSprite (targetId, optZipType) {
|
exportSprite (targetId, optZipType) {
|
||||||
const sb3 = require('./serialization/sb3');
|
|
||||||
|
|
||||||
const soundDescs = serializeSounds(this.runtime, targetId);
|
const soundDescs = serializeSounds(this.runtime, targetId);
|
||||||
const costumeDescs = serializeCostumes(this.runtime, targetId);
|
const costumeDescs = serializeCostumes(this.runtime, targetId);
|
||||||
const spriteJson = StringUtil.stringify(sb3.serialize(this.runtime, targetId));
|
const spriteJson = this.toJSON(targetId);
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
zip.file('sprite.json', spriteJson);
|
zip.file('sprite.json', spriteJson);
|
||||||
|
@ -448,12 +446,13 @@ class VirtualMachine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export project as a Scratch 3.0 JSON representation.
|
* Export project or sprite as a Scratch 3.0 JSON representation.
|
||||||
|
* @param {string=} optTargetId - Optional id of a sprite to serialize
|
||||||
* @return {string} Serialized state of the runtime.
|
* @return {string} Serialized state of the runtime.
|
||||||
*/
|
*/
|
||||||
toJSON () {
|
toJSON (optTargetId) {
|
||||||
const sb3 = require('./serialization/sb3');
|
const sb3 = require('./serialization/sb3');
|
||||||
return StringUtil.stringify(sb3.serialize(this.runtime));
|
return StringUtil.stringify(sb3.serialize(this.runtime, optTargetId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO do we still need this function? Keeping it here so as not to introduce
|
// TODO do we still need this function? Keeping it here so as not to introduce
|
||||||
|
|
BIN
test/fixtures/corrupt_svg.sb3
vendored
Normal file
BIN
test/fixtures/corrupt_svg.sb3
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/corrupt_svg.sprite3
vendored
Normal file
BIN
test/fixtures/corrupt_svg.sprite3
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/default.sb3
vendored
Normal file
BIN
test/fixtures/default.sb3
vendored
Normal file
Binary file not shown.
13
test/fixtures/fake-renderer.js
vendored
13
test/fixtures/fake-renderer.js
vendored
|
@ -4,6 +4,19 @@ const FakeRenderer = function () {
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
this.order = 0;
|
this.order = 0;
|
||||||
this.spriteCount = 5;
|
this.spriteCount = 5;
|
||||||
|
this._nextSkinId = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
FakeRenderer.prototype.createSVGSkin = function () {
|
||||||
|
return this._nextSkinId++;
|
||||||
|
};
|
||||||
|
|
||||||
|
FakeRenderer.prototype.getSkinSize = function (d) { // eslint-disable-line no-unused-vars
|
||||||
|
return [0, 0];
|
||||||
|
};
|
||||||
|
|
||||||
|
FakeRenderer.prototype.getSkinRotationCenter = function (d) { // eslint-disable-line no-unused-vars
|
||||||
|
return [0, 0];
|
||||||
};
|
};
|
||||||
|
|
||||||
FakeRenderer.prototype.createDrawable = function () {
|
FakeRenderer.prototype.createDrawable = function () {
|
||||||
|
|
4
test/fixtures/make-test-storage.js
vendored
4
test/fixtures/make-test-storage.js
vendored
|
@ -39,8 +39,8 @@ const getAssetUrl = function (asset) {
|
||||||
const makeTestStorage = function () {
|
const makeTestStorage = function () {
|
||||||
const storage = new ScratchStorage();
|
const storage = new ScratchStorage();
|
||||||
const AssetType = storage.AssetType;
|
const AssetType = storage.AssetType;
|
||||||
storage.addWebSource([AssetType.Project], getProjectUrl);
|
storage.addWebStore([AssetType.Project], getProjectUrl);
|
||||||
storage.addWebSource([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
|
storage.addWebStore([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
|
||||||
return storage;
|
return storage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
BIN
test/fixtures/missing_svg.sb3
vendored
Normal file
BIN
test/fixtures/missing_svg.sb3
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/missing_svg.sprite3
vendored
Normal file
BIN
test/fixtures/missing_svg.sprite3
vendored
Normal file
Binary file not shown.
5
test/fixtures/readProjectFile.js
vendored
5
test/fixtures/readProjectFile.js
vendored
|
@ -12,5 +12,10 @@ module.exports = {
|
||||||
return JSON.parse(zip.readAsText(projectEntry.entryName, 'utf8'));
|
return JSON.parse(zip.readAsText(projectEntry.entryName, 'utf8'));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
|
extractAsset: function (path, assetFileName) {
|
||||||
|
const zip = new AdmZip(path);
|
||||||
|
const assetEntry = zip.getEntries().filter(item => item.entryName.match(assetFileName))[0];
|
||||||
|
return assetEntry.getData();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
107
test/integration/sb3_corrupted_svg.js
Normal file
107
test/integration/sb3_corrupted_svg.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* This test mocks render breaking on loading a corrupted vector costume.
|
||||||
|
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||||
|
* but keeping track of the original costume data and serializing the
|
||||||
|
* original costume data back out. The saved project.json should not
|
||||||
|
* reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const md5 = require('js-md5');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg';
|
||||||
|
const originalCostume = extractAsset(projectUri, costumeFileName);
|
||||||
|
// We need to get the actual md5 because we hand modified the svg to corrupt it
|
||||||
|
// after we downloaded the project from Scratch
|
||||||
|
// Loading the project back into the VM will correct the assetId and md5
|
||||||
|
const brokenCostumeMd5 = md5(originalCostume);
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
let defaultVectorAssetId;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||||
|
|
||||||
|
// Mock renderer breaking on loading a corrupt costume
|
||||||
|
FakeRenderer.prototype.createSVGSkin = function (svgString) {
|
||||||
|
// Look for text added to costume to make it a corrupt svg
|
||||||
|
if (svgString.includes('<here is some')) {
|
||||||
|
throw new Error('mock createSVGSkin broke');
|
||||||
|
}
|
||||||
|
return FakeRenderer._nextSkinId++;
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
|
||||||
|
return vm.loadProject(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('load sb3 project with corrupted vector costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const blueGuySprite = vm.runtime.targets[1];
|
||||||
|
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||||
|
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = blueGuySprite.getCostumes()[0];
|
||||||
|
t.equal(corruptedCostume.name, 'costume1');
|
||||||
|
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(corruptedCostume.broken);
|
||||||
|
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||||
|
// Verify that we saved the original asset data
|
||||||
|
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save project with corrupted vector costume file', t => {
|
||||||
|
const resavedProject = JSON.parse(vm.toJSON());
|
||||||
|
|
||||||
|
t.equal(resavedProject.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = resavedProject.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const blueGuySprite = resavedProject.targets[1];
|
||||||
|
t.equal(blueGuySprite.name, 'Blue Square Guy');
|
||||||
|
t.equal(blueGuySprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = blueGuySprite.costumes[0];
|
||||||
|
t.equal(corruptedCostume.name, 'costume1');
|
||||||
|
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||||
|
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(corruptedCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume saves orignal broken costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id);
|
||||||
|
t.equal(costumeDescs.length, 1);
|
||||||
|
const costume = costumeDescs[0];
|
||||||
|
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
|
||||||
|
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
87
test/integration/sb3_missing_svg.js
Normal file
87
test/integration/sb3_missing_svg.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* This test ensures that the VM gracefully handles an sb3 project with
|
||||||
|
* a missing vector costume. The VM should handle this safely by displaying
|
||||||
|
* a Gray Question Mark, but keeping track of the original costume data
|
||||||
|
* and serializing the original costume data back out. The saved project.json
|
||||||
|
* should not reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb';
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
// This line removes the webhelper from the list of available helpers.
|
||||||
|
// W/o the following line, this fails because storage doesn't handle the case
|
||||||
|
// where none of the tools have isGetSupported: true
|
||||||
|
// TODO: Remove this line when the related storage bug is resolved so that
|
||||||
|
// storage gracefully handles non-browser situations where assets are missing.
|
||||||
|
storage._helpers = [storage._helpers[0]];
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
|
||||||
|
return vm.loadProject(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('loading sb3 project with missing vector costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const blueGuySprite = vm.runtime.targets[1];
|
||||||
|
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||||
|
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const missingCostume = blueGuySprite.getCostumes()[0];
|
||||||
|
t.equal(missingCostume.name, 'costume1');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||||
|
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'svg');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(missingCostume.broken);
|
||||||
|
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sb3 project with missing costume file', t => {
|
||||||
|
const resavedProject = JSON.parse(vm.toJSON());
|
||||||
|
|
||||||
|
t.equal(resavedProject.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = resavedProject.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const blueGuySprite = resavedProject.targets[1];
|
||||||
|
t.equal(blueGuySprite.name, 'Blue Square Guy');
|
||||||
|
t.equal(blueGuySprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const missingCostume = blueGuySprite.costumes[0];
|
||||||
|
t.equal(missingCostume.name, 'costume1');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'svg');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(missingCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
106
test/integration/sprite3_corrupted_svg.js
Normal file
106
test/integration/sprite3_corrupted_svg.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/**
|
||||||
|
* This test mocks render breaking on loading a sprite with a
|
||||||
|
* corrupted vector costume.
|
||||||
|
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||||
|
* but keeping track of the original costume data and serializing the
|
||||||
|
* original costume data back out. The saved project.json should not
|
||||||
|
* reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const md5 = require('js-md5');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sprite3');
|
||||||
|
const sprite = readFileToBuffer(spriteUri);
|
||||||
|
|
||||||
|
const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg';
|
||||||
|
const originalCostume = extractAsset(spriteUri, costumeFileName);
|
||||||
|
// We need to get the actual md5 because we hand modified the svg to corrupt it
|
||||||
|
// after we downloaded the project from Scratch
|
||||||
|
// Loading the project back into the VM will correct the assetId and md5
|
||||||
|
const brokenCostumeMd5 = md5(originalCostume);
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
let defaultVectorAssetId;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||||
|
|
||||||
|
// Mock renderer breaking on loading a corrupt costume
|
||||||
|
FakeRenderer.prototype.createSVGSkin = function (svgString) {
|
||||||
|
// Look for text added to costume to make it a corrupt svg
|
||||||
|
if (svgString.includes('<here is some')) {
|
||||||
|
throw new Error('mock createSVGSkin broke');
|
||||||
|
}
|
||||||
|
return FakeRenderer.prototype._nextSkinId++;
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
|
||||||
|
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('load sprite3 with corrupted vector costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 3);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const blueGuySprite = vm.runtime.targets[2];
|
||||||
|
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||||
|
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = blueGuySprite.getCostumes()[0];
|
||||||
|
t.equal(corruptedCostume.name, 'costume1');
|
||||||
|
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(corruptedCostume.broken);
|
||||||
|
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||||
|
// Verify that we saved the original asset data
|
||||||
|
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sprite with corrupted costume file', t => {
|
||||||
|
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||||
|
|
||||||
|
t.equal(resavedSprite.name, 'Blue Square Guy');
|
||||||
|
t.equal(resavedSprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = resavedSprite.costumes[0];
|
||||||
|
t.equal(corruptedCostume.name, 'costume1');
|
||||||
|
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||||
|
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'svg');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(corruptedCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume saves orignal broken costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||||
|
t.equal(costumeDescs.length, 1);
|
||||||
|
const costume = costumeDescs[0];
|
||||||
|
t.equal(costume.fileName, `${brokenCostumeMd5}.svg`);
|
||||||
|
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
85
test/integration/sprite3_missing_svg.js
Normal file
85
test/integration/sprite3_missing_svg.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* This test ensures that the VM gracefully handles a sprite3 file with
|
||||||
|
* a missing vector costume. The VM should handle this safely by displaying
|
||||||
|
* a Gray Question Mark, but keeping track of the original costume data
|
||||||
|
* and serializing the original costume data back out. The saved project.json
|
||||||
|
* should not reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
|
||||||
|
// The particular project that we're loading doesn't matter for this test
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
const spriteUri = path.resolve(__dirname, '../fixtures/missing_svg.sprite3');
|
||||||
|
const sprite = readFileToBuffer(spriteUri);
|
||||||
|
|
||||||
|
const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb';
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
// This line removes the webhelper from the list of available helpers.
|
||||||
|
// W/o the following line, this fails because storage doesn't handle the case
|
||||||
|
// where none of the tools have isGetSupported: true
|
||||||
|
// TODO: Remove this line when the related storage bug is resolved so that
|
||||||
|
// storage gracefully handles non-browser situations where assets are missing.
|
||||||
|
storage._helpers = [storage._helpers[0]];
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
|
||||||
|
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('loading sprite3 with missing vector costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 3);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const blueGuySprite = vm.runtime.targets[2];
|
||||||
|
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
|
||||||
|
t.equal(blueGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const missingCostume = blueGuySprite.getCostumes()[0];
|
||||||
|
t.equal(missingCostume.name, 'costume1');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
|
||||||
|
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'svg');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(missingCostume.broken);
|
||||||
|
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sprite3 with missing vector costume file', t => {
|
||||||
|
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||||
|
|
||||||
|
t.equal(resavedSprite.name, 'Blue Square Guy');
|
||||||
|
t.equal(resavedSprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const missingCostume = resavedSprite.costumes[0];
|
||||||
|
t.equal(missingCostume.name, 'costume1');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'svg');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(missingCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue