diff --git a/package.json b/package.json index b31d30af2..0308aa6f7 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "scratch-audio": "^0.1.0-prerelease.0", "scratch-blocks": "^0.1.0-prerelease.0", "scratch-render": "^0.1.0-prerelease.0", - "scratch-storage": "^0.0.1-prerelease.0", + "scratch-storage": "^0.1.0", "script-loader": "0.7.0", "stats.js": "^0.17.0", "tap": "^10.2.0", diff --git a/src/import/load-costume.js b/src/import/load-costume.js index 5d9fdc1d5..a9323398d 100644 --- a/src/import/load-costume.js +++ b/src/import/load-costume.js @@ -30,7 +30,8 @@ const loadCostume = function (md5ext, costume, runtime) { ]; let promise = runtime.storage.load(assetType, md5).then(costumeAsset => { - costume.url = costumeAsset.encodeDataURI(); + costume.assetId = costumeAsset.assetId; + costume.assetType = assetType; return costumeAsset; }); diff --git a/src/import/load-sound.js b/src/import/load-sound.js index f54f5fe32..a308e1516 100644 --- a/src/import/load-sound.js +++ b/src/import/load-sound.js @@ -19,10 +19,17 @@ const loadSound = function (sound, runtime) { } const idParts = sound.md5.split('.'); const md5 = idParts[0]; - return runtime.storage.load(runtime.storage.AssetType.Sound, md5).then(soundAsset => { - sound.data = soundAsset.data; - return runtime.audioEngine.decodeSound(sound).then(() => sound); - }); + return runtime.storage.load(runtime.storage.AssetType.Sound, md5) + .then(soundAsset => { + sound.assetId = soundAsset.assetId; + sound.assetType = runtime.storage.AssetType.Sound; + return runtime.audioEngine.decodeSound(Object.assign( + {}, + sound, + {data: soundAsset.data} + )); + }) + .then(() => sound); }; module.exports = loadSound; diff --git a/src/playground/index.html b/src/playground/index.html index 5f293e9b4..d248b9847 100644 --- a/src/playground/index.html +++ b/src/playground/index.html @@ -53,9 +53,9 @@

- +   - +

@@ -71,18 +71,17 @@ diff --git a/src/import/sb2import.js b/src/serialization/sb2.js similarity index 98% rename from src/import/sb2import.js rename to src/serialization/sb2.js index 4abd6230b..a54b582f2 100644 --- a/src/import/sb2import.js +++ b/src/serialization/sb2.js @@ -11,12 +11,12 @@ const Sprite = require('../sprites/sprite'); const Color = require('../util/color'); const log = require('../util/log'); const uid = require('../util/uid'); -const specMap = require('./sb2specmap'); +const specMap = require('./sb2_specmap'); const Variable = require('../engine/variable'); const List = require('../engine/list'); -const loadCostume = require('./load-costume.js'); -const loadSound = require('./load-sound.js'); +const loadCostume = require('../import/load-costume.js'); +const loadSound = require('../import/load-sound.js'); /** * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") @@ -276,14 +276,14 @@ const parseScratchObject = function (object, runtime, topLevel) { /** * Top-level handler. Parse provided JSON, * and process the top-level object (the stage object). - * @param {!string} json SB2-format JSON to load. + * @param {!object} json SB2-format JSON to load. * @param {!Runtime} runtime Runtime object to load all structures into. * @param {boolean=} optForceSprite If set, treat as sprite (Sprite2). * @return {?Promise} Promise that resolves to the loaded targets when ready. */ const sb2import = function (json, runtime, optForceSprite) { return parseScratchObject( - JSON.parse(json), + json, runtime, !optForceSprite ); @@ -470,4 +470,6 @@ const parseBlock = function (sb2block) { return activeBlock; }; -module.exports = sb2import; +module.exports = { + deserialize: sb2import +}; diff --git a/src/import/sb2specmap.js b/src/serialization/sb2_specmap.js similarity index 100% rename from src/import/sb2specmap.js rename to src/serialization/sb2_specmap.js diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js new file mode 100644 index 000000000..4477a7b51 --- /dev/null +++ b/src/serialization/sb3.js @@ -0,0 +1,165 @@ +/** + * @fileoverview + * Partial implementation of a SB3 serializer and deserializer. Parses provided + * JSON and then generates all needed scratch-vm runtime structures. + */ + +const vmPackage = require('../../package.json'); +const Blocks = require('../engine/blocks'); +const Sprite = require('../sprites/sprite'); +const Variable = require('../engine/variable'); +const List = require('../engine/list'); + +const loadCostume = require('../import/load-costume.js'); +const loadSound = require('../import/load-sound.js'); + +/** + * Serializes the specified VM runtime. + * @param {!Runtime} runtime VM runtime instance to be serialized. + * @return {object} Serialized runtime instance. + */ +const serialize = function (runtime) { + // Fetch targets + const obj = Object.create(null); + obj.targets = runtime.targets; + + // Assemble metadata + const meta = Object.create(null); + meta.semver = '3.0.0'; + meta.vm = vmPackage.version; + + // Attach full user agent string to metadata if available + meta.agent = null; + if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent; + + // Assemble payload and return + obj.meta = meta; + return obj; +}; + +/** + * Parse a single "Scratch object" and create all its in-memory VM objects. + * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher. + * @param {!Runtime} runtime Runtime object to load all structures into. + * @return {?Target} Target created (stage or sprite). + */ +const parseScratchObject = function (object, runtime) { + if (!object.hasOwnProperty('name')) { + // Watcher/monitor - skip this object until those are implemented in VM. + // @todo + return; + } + // Blocks container for this object. + const blocks = new Blocks(); + + // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. + const sprite = new Sprite(blocks, runtime); + + // Sprite/stage name from JSON. + if (object.hasOwnProperty('name')) { + sprite.name = object.name; + } + if (object.hasOwnProperty('blocks')) { + for (let blockId in object.blocks) { + blocks.createBlock(object.blocks[blockId]); + } + // console.log(blocks); + } + // Costumes from JSON. + const costumePromises = (object.costumes || []).map(costumeSource => { + // @todo: Make sure all the relevant metadata is being pulled out. + const costume = { + skinId: null, + name: costumeSource.name, + bitmapResolution: costumeSource.bitmapResolution, + rotationCenterX: costumeSource.rotationCenterX, + rotationCenterY: costumeSource.rotationCenterY + }; + const costumeMd5 = `${costumeSource.assetId}.${costumeSource.assetType.runtimeFormat}`; + return loadCostume(costumeMd5, costume, runtime); + }); + // Sounds from JSON + const soundPromises = (object.sounds || []).map(soundSource => { + const sound = { + format: soundSource.format, + fileUrl: soundSource.fileUrl, + rate: soundSource.rate, + sampleCount: soundSource.sampleCount, + soundID: soundSource.soundID, + name: soundSource.name, + md5: soundSource.md5, + data: null + }; + return loadSound(sound, runtime); + }); + // Create the first clone, and load its run-state from JSON. + const target = sprite.createClone(); + // Load target properties from JSON. + if (object.hasOwnProperty('variables')) { + for (let j = 0; j < object.variables.length; j++) { + const variable = object.variables[j]; + target.variables[variable.name] = new Variable( + variable.name, + variable.value, + variable.isPersistent + ); + } + } + if (object.hasOwnProperty('lists')) { + for (let k = 0; k < object.lists.length; k++) { + const list = object.lists[k]; + // @todo: monitor properties. + target.lists[list.listName] = new List( + list.listName, + list.contents + ); + } + } + if (object.hasOwnProperty('x')) { + target.x = object.x; + } + if (object.hasOwnProperty('y')) { + target.y = object.y; + } + if (object.hasOwnProperty('direction')) { + target.direction = object.direction; + } + if (object.hasOwnProperty('size')) { + target.size = object.size; + } + if (object.hasOwnProperty('visible')) { + target.visible = object.visible; + } + if (object.hasOwnProperty('currentCostume')) { + target.currentCostume = object.currentCostume; + } + if (object.hasOwnProperty('rotationStyle')) { + target.rotationStyle = object.rotationStyle; + } + if (object.hasOwnProperty('isStage')) { + target.isStage = object.isStage; + } + Promise.all(costumePromises).then(costumes => { + sprite.costumes = costumes; + }); + Promise.all(soundPromises).then(sounds => { + sprite.sounds = sounds; + }); + return Promise.all(costumePromises.concat(soundPromises)).then(() => target); +}; + +/** + * Deserializes the specified representation of a VM runtime and loads it into + * the provided runtime instance. + * @param {object} json JSON representation of a VM runtime. + * @param {Runtime} runtime Runtime instance + * @returns {Promise} Promise that resolves to the list of targets after the project is deserialized + */ +const deserialize = function (json, runtime) { + return Promise.all((json.targets || []).map(target => parseScratchObject(target, runtime))); +}; + +module.exports = { + serialize: serialize, + deserialize: deserialize +}; diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index ad23003c7..1ffc00a1e 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -764,11 +764,13 @@ class RenderedTarget extends Target { this.dragging = false; } + /** * Serialize sprite info, used when emitting events about the sprite - * @returns {object} sprite data as a simple object + * @returns {object} Sprite data as a simple object */ toJSON () { + const costumes = this.getCostumes(); return { id: this.id, name: this.getName(), @@ -778,12 +780,16 @@ class RenderedTarget extends Target { size: this.size, direction: this.direction, draggable: this.draggable, - costume: this.getCurrentCostume(), - costumes: this.getCostumes(), - sounds: this.getSounds(), - costumeCount: this.getCostumes().length, + currentCostume: this.currentCostume, + costume: costumes[this.currentCostume], + costumeCount: costumes.length, visible: this.visible, - rotationStyle: this.rotationStyle + rotationStyle: this.rotationStyle, + blocks: this.blocks._blocks, + variables: this.variables, + lists: this.lists, + costumes: costumes, + sounds: this.getSounds() }; } diff --git a/src/virtual-machine.js b/src/virtual-machine.js index b39d74d8a..719bf6cc7 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -2,7 +2,8 @@ const EventEmitter = require('events'); const log = require('./util/log'); const Runtime = require('./engine/runtime'); -const sb2import = require('./import/sb2import'); +const sb2 = require('./serialization/sb2'); +const sb3 = require('./serialization/sb3'); const StringUtil = require('./util/string-util'); const loadCostume = require('./import/load-costume.js'); @@ -145,22 +146,7 @@ class VirtualMachine extends EventEmitter { */ loadProject (json) { // @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0. - return sb2import(json, this.runtime).then(targets => { - this.clear(); - for (let n = 0; n < targets.length; n++) { - if (targets[n] !== null) { - this.runtime.targets.push(targets[n]); - targets[n].updateAllDrawableProperties(); - } - } - // Select the first target for editing, e.g., the first sprite. - this.editingTarget = this.runtime.targets[1]; - - // Update the VM user's knowledge of targets and blocks on the workspace. - this.emitTargetsUpdate(); - this.emitWorkspaceUpdate(); - this.runtime.setEditingTarget(this.editingTarget); - }); + return this.fromJSON(json); } /** @@ -180,12 +166,95 @@ class VirtualMachine extends EventEmitter { }); } + /** + * @returns {string} Project in a Scratch 3.0 JSON representation. + */ + saveProjectSb3 () { + // @todo: Handle other formats, e.g., Scratch 1.4, Scratch 2.0. + return this.toJSON(); + } + + /** + * Export project as a Scratch 3.0 JSON representation. + * @return {string} Serialized state of the runtime. + */ + toJSON () { + return JSON.stringify(sb3.serialize(this.runtime)); + } + + /** + * Load a project from a Scratch JSON representation. + * @param {string} json JSON string representing a project. + * @returns {Promise} Promise that resolves after the project has loaded + */ + fromJSON (json) { + // Clear the current runtime + this.clear(); + + // Validate & parse + if (typeof json !== 'string') { + log.error('Failed to parse project. Non-string supplied to fromJSON.'); + return; + } + json = JSON.parse(json); + if (typeof json !== 'object') { + log.error('Failed to parse project. JSON supplied to fromJSON is not an object.'); + return; + } + + // Establish version, deserialize, and load into runtime + // @todo Support Scratch 1.4 + // @todo This is an extremely naïve / dangerous way of determining version. + // See `scratch-parser` for a more sophisticated validation + // methodology that should be adapted for use here + let deserializer; + if ((typeof json.meta !== 'undefined') && (typeof json.meta.semver !== 'undefined')) { + deserializer = sb3; + } else { + deserializer = sb2; + } + + return deserializer.deserialize(json, this.runtime).then(targets => { + this.clear(); + for (let n = 0; n < targets.length; n++) { + if (targets[n] !== null) { + this.runtime.targets.push(targets[n]); + targets[n].updateAllDrawableProperties(); + } + } + // Select the first target for editing, e.g., the first sprite. + if (this.runtime.targets.length > 1) { + this.editingTarget = this.runtime.targets[1]; + } else { + this.editingTarget = this.runtime.targets[0]; + } + + // Update the VM user's knowledge of targets and blocks on the workspace. + this.emitTargetsUpdate(); + this.emitWorkspaceUpdate(); + this.runtime.setEditingTarget(this.editingTarget); + }); + } + /** * Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format. * @param {string} json JSON string representing the sprite. + * @returns {Promise} Promise that resolves after the sprite is added */ addSprite2 (json) { - sb2import(json, this.runtime, true).then(targets => { + // Validate & parse + if (typeof json !== 'string') { + log.error('Failed to parse sprite. Non-string supplied to addSprite2.'); + return; + } + json = JSON.parse(json); + if (typeof json !== 'object') { + log.error('Failed to parse sprite. JSON supplied to addSprite2 is not an object.'); + return; + } + + // Select new sprite. + return sb2.deserialize(json, this.runtime, true).then(targets => { this.runtime.targets.push(targets[0]); this.editingTarget = targets[0]; this.editingTarget.updateAllDrawableProperties(); diff --git a/test/fixtures/demo.json b/test/fixtures/demo.json new file mode 100644 index 000000000..d54626da6 --- /dev/null +++ b/test/fixtures/demo.json @@ -0,0 +1 @@ +{"targets":[{"id":"poXv!.~0@*~j*F(Om/X5","name":"Stage","isStage":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"currentCostume":10,"costume":{"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":7,"assetId":"7da4181ee167de7b3f5d1a91880277ff","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},"costumeCount":11,"visible":true,"rotationStyle":"all around","blocks":{"O7D.%E~TH^ULpAuHM8)@":{"id":"O7D.%E~TH^ULpAuHM8)@","opcode":"event_whenflagclicked","inputs":{},"fields":{},"next":"78+=E[P7b8!mBXMwQF`@","shadow":false,"x":138,"y":356.40000000000003,"topLevel":true,"parent":null},"78+=E[P7b8!mBXMwQF`@":{"id":"78+=E[P7b8!mBXMwQF`@","opcode":"control_forever","inputs":{"SUBSTACK":{"name":"SUBSTACK","block":"nlnk{2XqtVI*Qg,admbW","shadow":null}},"fields":{},"next":null,"shadow":false,"parent":"O7D.%E~TH^ULpAuHM8)@"},"nlnk{2XqtVI*Qg,admbW":{"id":"nlnk{2XqtVI*Qg,admbW","opcode":"looks_changeeffectby","inputs":{"EFFECT":{"name":"EFFECT","block":"*vh;qV87Q}5IP@sW=)wD","shadow":"*vh;qV87Q}5IP@sW=)wD"},"CHANGE":{"name":"CHANGE","block":"woYo[[v=PD(`R;qW{PZ%","shadow":"woYo[[v=PD(`R;qW{PZ%"}},"fields":{},"next":null,"shadow":false,"parent":"78+=E[P7b8!mBXMwQF`@"},"*vh;qV87Q}5IP@sW=)wD":{"id":"*vh;qV87Q}5IP@sW=)wD","opcode":"looks_effectmenu","inputs":{},"fields":{"EFFECT":{"name":"EFFECT","value":"color"}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true},"woYo[[v=PD(`R;qW{PZ%":{"id":"woYo[[v=PD(`R;qW{PZ%","opcode":"math_number","inputs":{},"fields":{"NUM":{"name":"NUM","value":1}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true}},"variables":{"vy":{"name":"vy","value":-3.5165000000000006,"isCloud":false},"g":{"name":"g","value":"-.5","isCloud":false},"vx":{"name":"vx","value":1.4580000000000002,"isCloud":false},"i":{"name":"i","value":3,"isCloud":false},"d1":{"name":"d1","value":0,"isCloud":false},"x":{"name":"x","value":233.9173278872492,"isCloud":false},"acceleration":{"name":"acceleration","value":"-3","isCloud":false}},"lists":{},"costumes":[{"name":"blue sky2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":0,"assetId":"7623e679b2baa2e7d48808614820844f","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"woods","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":3,"assetId":"1e0f7a4c932423b13250b5cb44928109","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"party","bitmapResolution":1,"rotationCenterX":251,"rotationCenterY":189,"skinId":2,"assetId":"108160d0e44d1c340182e31c9dc0758a","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"boardwalk","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":6,"assetId":"de0e54cd11551566f044e7e6bc588b2c","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"blue sky3","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":179,"skinId":1,"assetId":"2024d59c1980e667e8f656134796e2c1","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"underwater1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":5,"assetId":"f339c6f31b11ea71d0fb8d607cec392e","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"underwater2","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":11,"assetId":"1517c21786d2d0edc2f3037408d850bd","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"stars","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":4,"assetId":"e87fed9c2a968dbeae8c94617e600e8c","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"parking-ramp","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":9,"assetId":"a7832479977c166ca0057f2a99a73305","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"backdrop1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":10,"assetId":"f67dc7de38bac6fbb0ab68e46352521d","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":7,"assetId":"7da4181ee167de7b3f5d1a91880277ff","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}}],"sounds":[{"name":"pop","format":"","rate":11025,"sampleCount":258,"soundID":-1,"md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","assetType":{"contentType":"audio/x-wav","name":"Sound","runtimeFormat":"wav"}}]},{"id":"9!d8G^[6i]k*:l[W%4;l","name":"Earth","isStage":false,"x":-10,"y":10,"size":100,"direction":90,"draggable":false,"currentCostume":0,"costume":{"name":"earth","bitmapResolution":1,"rotationCenterX":72,"rotationCenterY":72,"skinId":8,"assetId":"814197522984a302972998b1a7f92d91","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},"costumeCount":1,"visible":true,"rotationStyle":"all around","blocks":{},"variables":{},"lists":{},"costumes":[{"name":"earth","bitmapResolution":1,"rotationCenterX":72,"rotationCenterY":72,"skinId":8,"assetId":"814197522984a302972998b1a7f92d91","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}}],"sounds":[{"name":"pop","format":"","rate":11025,"sampleCount":258,"soundID":-1,"md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","assetType":{"contentType":"audio/x-wav","name":"Sound","runtimeFormat":"wav"}}]}],"meta":{"semver":"3.0.0","vm":"0.1.0","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"}} diff --git a/test/integration/import_sb2.js b/test/integration/import_sb2.js index 547b645ea..3b375a44c 100644 --- a/test/integration/import_sb2.js +++ b/test/integration/import_sb2.js @@ -5,10 +5,10 @@ const extract = require('../fixtures/extract'); const renderedTarget = require('../../src/sprites/rendered-target'); const runtime = require('../../src/engine/runtime'); -const sb2 = require('../../src/import/sb2import'); +const sb2 = require('../../src/serialization/sb2'); test('spec', t => { - t.type(sb2, 'function'); + t.type(sb2.deserialize, 'function'); t.end(); }); @@ -16,13 +16,15 @@ test('default', t => { // Get SB2 JSON (string) const uri = path.resolve(__dirname, '../fixtures/default.sb2'); const file = extract(uri); + const json = JSON.parse(file); // Create runtime instance & load SB2 into it const rt = new runtime(); attachTestStorage(rt); - sb2(file, rt).then(targets => { + sb2.deserialize(json, rt).then(targets => { // Test t.type(file, 'string'); + t.type(json, 'object'); t.type(rt, 'object'); t.type(targets, 'object'); diff --git a/test/unit/serialization_sb2.js b/test/unit/serialization_sb2.js new file mode 100644 index 000000000..cd3294f0a --- /dev/null +++ b/test/unit/serialization_sb2.js @@ -0,0 +1,53 @@ +const path = require('path'); +const test = require('tap').test; +const extract = require('../fixtures/extract'); + +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 file = extract(uri); + const json = JSON.parse(file); + + // Create runtime instance & load SB2 into it + const rt = new Runtime(); + sb2.deserialize(json, rt).then(targets => { + // Test + t.type(file, 'string'); + 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].lists, '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].lists, '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(); + }); +}); diff --git a/test/unit/serialization_sb3.js b/test/unit/serialization_sb3.js new file mode 100644 index 000000000..a43ce204f --- /dev/null +++ b/test/unit/serialization_sb3.js @@ -0,0 +1,22 @@ +const test = require('tap').test; +const VirtualMachine = require('../../src/index'); +const sb3 = require('../../src/serialization/sb3'); +const demoSb3 = require('../fixtures/demo.json'); + +test('serialize', t => { + const vm = new VirtualMachine(); + vm.fromJSON(JSON.stringify(demoSb3)); + 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 Analyize + t.type(targets, 'object'); + t.end(); + }); +});