UNPKG

pxt-core

Version:

Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors

257 lines (256 loc) • 9.06 kB
/// <reference path="..\..\localtypings\pxtblockly.d.ts" /> /// <reference path="..\..\built\pxtblocks.d.ts" /> /// <reference path="..\..\built\pxtcompiler.d.ts" /> /// <reference path="..\..\built\pxteditor.d.ts" /> const WEB_PREFIX = "http://localhost:9876"; // Blockly crashes if this isn't defined Blockly.Msg.DELETE_VARIABLE = "Delete the '%1' variable"; // target.js should be embedded in the page pxt.setAppTarget(window.pxtTargetBundle); // Webworker needs this config to run pxt.webConfig = { relprefix: undefined, verprefix: undefined, workerjs: WEB_PREFIX + "/blb/worker.js", monacoworkerjs: undefined, gifworkerjs: undefined, serviceworkerjs: undefined, typeScriptWorkerJs: undefined, pxtVersion: undefined, pxtRelId: undefined, pxtCdnUrl: undefined, commitCdnUrl: undefined, blobCdnUrl: undefined, cdnUrl: undefined, targetVersion: undefined, targetRelId: undefined, targetUrl: undefined, targetId: undefined, simUrl: undefined, simserviceworkerUrl: undefined, simworkerconfigUrl: undefined, partsUrl: undefined, runUrl: undefined, docsUrl: undefined, multiUrl: undefined, isStatic: undefined, }; class BlocklyCompilerTestHost { constructor() { } static createTestHostAsync() { if (pxt.appTarget.appTheme && pxt.appTarget.appTheme.extendFieldEditors && pxt.editor.initFieldExtensionsAsync) { return pxt.editor.initFieldExtensionsAsync({}) .then(res => { if (res.fieldEditors) res.fieldEditors.forEach(fi => { pxt.blocks.registerFieldEditor(fi.selector, fi.editor, fi.validator); }); }) .then(() => new BlocklyCompilerTestHost()); } return Promise.resolve(new BlocklyCompilerTestHost()); } readFile(module, filename) { if (module.id == "this" && filename == "pxt.json") { return JSON.stringify(pxt.appTarget.blocksprj.config); } const bundled = pxt.getEmbeddedScript(module.id); if (bundled) { return bundled[filename]; } return ""; } writeFile(module, filename, contents) { if (filename == pxt.CONFIG_NAME) return; // ignore config writes throw ts.pxtc.Util.oops("trying to write " + module + " / " + filename); } getHexInfoAsync(extInfo) { return pxt.hexloader.getHexInfoAsync(this, extInfo); } cacheStoreAsync(id, val) { return Promise.resolve(); } cacheGetAsync(id) { return Promise.resolve(null); } downloadPackageAsync(pkg) { return Promise.resolve(); } } BlocklyCompilerTestHost.cachedFiles = {}; // @ts-ignore function fail(msg) { chai.assert(false, msg); } let cachedBlocksInfo; function getBlocksInfoAsync() { if (cachedBlocksInfo) { return Promise.resolve(cachedBlocksInfo); } return BlocklyCompilerTestHost.createTestHostAsync() .then(host => { const pkg = new pxt.MainPackage(host); return pkg.getCompileOptionsAsync(); }, err => fail('Unable to create test host')) .then(opts => { opts.ast = true; let resp = pxtc.compile(opts); if (resp.diagnostics && resp.diagnostics.length > 0) resp.diagnostics.forEach(diag => console.error(diag.messageText)); if (!resp.success) return Promise.reject("Could not compile"); // decompile to blocks let apis = pxtc.getApiInfo(resp.ast, opts.jres); let blocksInfo = pxtc.getBlocksInfo(apis); pxt.blocks.initializeAndInject(blocksInfo); cachedBlocksInfo = blocksInfo; return cachedBlocksInfo; }, err => fail('Could not get compile options')); } function testXmlAsync(blocksfile) { return initAsync() .then(() => getBlocksInfoAsync()) .then(blocksInfo => { const workspace = new Blockly.Workspace(); Blockly.mainWorkspace = workspace; const xml = Blockly.Xml.textToDom(blocksfile); try { Blockly.Xml.domToWorkspace(xml, workspace); } catch (e) { if (e.message && e.message.indexOf("isConnected") !== -1) { fail("Could not build workspace, this usually means a blockId (aka blockly 'type') changed"); } fail(e.message); } const err = compareBlocklyTrees(xml, Blockly.Xml.workspaceToDom(workspace)); if (err) { fail(`XML mismatch (${err.reason}) ${err.chain} \n See https://makecode.com/develop/blockstests for more info`); } }, err => fail(`Unable to get block info: ` + JSON.stringify(err))); } function mkLink(el) { let tag = el.tagName.toLowerCase(); switch (tag) { case "block": case "shadow": return `<${tag} type="${el.getAttribute("type")}">`; case "value": case "statement": case "title": case "field": return `<${tag} name="${el.getAttribute("name")}">`; default: return `<${tag}>`; ; } } function compareBlocklyTrees(a, b, prev = []) { prev.push(mkLink(a)); if (!shallowEquals(a, b)) return { chain: prev.join(" -> "), reason: "mismatched element" }; for (let i = 0; i < a.childNodes.length; i++) { const childA = a.childNodes.item(i); if (childA.nodeType === Node.ELEMENT_NODE) { const childB = getMatchingChild(childA, b); if (!childB) return { chain: prev.join(" -> "), reason: "missing child " + mkLink(childA) }; const err = compareBlocklyTrees(childA, childB, prev.slice()); if (err) { return err; } } } return undefined; } function getMatchingChild(searchFor, parent) { for (let i = 0; i < parent.childNodes.length; i++) { const child = parent.childNodes.item(i); if (child.nodeType === Node.ELEMENT_NODE && shallowEquals(searchFor, child)) return child; } return undefined; } function shallowEquals(a, b) { if (a.tagName.toLowerCase() !== b.tagName.toLowerCase()) return false; switch (a.tagName.toLowerCase()) { case "block": case "shadow": return a.getAttribute("type") === b.getAttribute("type"); case "value": case "statement": return a.getAttribute("name") === b.getAttribute("name"); case "title": case "field": return a.getAttribute("name") === b.getAttribute("name") && a.textContent.trim() === b.textContent.trim(); case "mutation": const aAttributes = a.attributes; const bAttributes = b.attributes; if (aAttributes.length !== bAttributes.length) return false; for (let i = 0; i < aAttributes.length; i++) { const attrName = aAttributes.item(i).name; if (a.getAttribute(attrName) != b.getAttribute(attrName)) return false; } return a.textContent.trim() === b.textContent.trim(); case "data": return a.textContent.trim() === b.textContent.trim(); case "next": case "comment": return true; } return true; } let init = false; function initAsync() { if (init) return Promise.resolve(); init = true; if (pxt.appTarget.appTheme && pxt.appTarget.appTheme.extendFieldEditors && pxt.editor.initFieldExtensionsAsync) { return pxt.editor.initFieldExtensionsAsync({}) .then(res => { if (res.fieldEditors) res.fieldEditors.forEach(fi => { pxt.blocks.registerFieldEditor(fi.selector, fi.editor, fi.validator); }); }); } return Promise.resolve(); } function encode(testcase) { return testcase.split(/[\\\/]/g).map(encodeURIComponent).join("/"); } if (testJSON.libsTests && testJSON.libsTests.length) { describe("block tests in target", function () { this.timeout(5000); for (const test of testJSON.libsTests) { describe("for package " + test.packageName, () => { for (const testFile of test.testFiles) { it("file " + testFile.testName, () => testXmlAsync(testFile.contents)); } }); } }); } if (testJSON.commonTests && testJSON.commonTests.length) { describe("block tests in common-packages", function () { this.timeout(5000); for (const test of testJSON.commonTests) { describe("for package " + test.packageName, () => { for (const testFile of test.testFiles) { it("file " + testFile.testName, () => testXmlAsync(testFile.contents)); } }); } }); }