|
| 1 | +import assert from "node:assert/strict"; |
| 2 | +import fs from "node:fs"; |
| 3 | +import { createRequire } from "node:module"; |
| 4 | +import path from "node:path"; |
| 5 | +import test from "node:test"; |
| 6 | +import { fileURLToPath } from "node:url"; |
| 7 | +import vm from "node:vm"; |
| 8 | + |
| 9 | +// Runs the real Blocks block definitions, code generator and workspace checks |
| 10 | +// in a headless Blockly workspace (no browser page). |
| 11 | +const require = createRequire(import.meta.url); |
| 12 | +const toolingDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 13 | +const repositoryDirectory = path.resolve(toolingDirectory, ".."); |
| 14 | +const Blockly = require(path.join(toolingDirectory, "node_modules", "blockly")); |
| 15 | +const core = require(path.join(toolingDirectory, "shared", "ser-language-core.js")); |
| 16 | +const { SER_TRUTH_TABLE } = require( |
| 17 | + path.join(repositoryDirectory, "VS Code Extension", "out", "ser_method_info.js") |
| 18 | +); |
| 19 | +const editorSource = fs.readFileSync( |
| 20 | + path.join(toolingDirectory, "visual-editor", "src", "editor.js"), |
| 21 | + "utf8" |
| 22 | +); |
| 23 | + |
| 24 | +function sliceBetween(source, startMarker, endMarker) { |
| 25 | + const start = startMarker ? source.indexOf(startMarker) : 0; |
| 26 | + const end = source.indexOf(endMarker); |
| 27 | + assert.ok(start >= 0 && end > start, `editor.js no longer contains ${startMarker || "its start"}…${endMarker}`); |
| 28 | + return source.slice(start, end); |
| 29 | +} |
| 30 | + |
| 31 | +function createHeadlessEditor() { |
| 32 | + const definitions = sliceBetween(editorSource, null, " function shadow("); |
| 33 | + const checks = sliceBetween(editorSource, " function entryAncestor(", " function renderDiagnostics("); |
| 34 | + const context = { Blockly, SER_TRUTH_TABLE, SERLanguageCore: core, console }; |
| 35 | + vm.createContext(context); |
| 36 | + vm.runInContext( |
| 37 | + `${definitions} |
| 38 | + const workspace = new Blockly.Workspace(); |
| 39 | + globalThis.workspace = workspace; |
| 40 | + globalThis.generator = serGenerator; |
| 41 | + ${checks} |
| 42 | + globalThis.check = workspaceDiagnostics; |
| 43 | + })();`, |
| 44 | + context |
| 45 | + ); |
| 46 | + return context; |
| 47 | +} |
| 48 | + |
| 49 | +function addRoot(editor, type, y, message, fields = {}) { |
| 50 | + const root = editor.workspace.newBlock(type); |
| 51 | + for (const [name, value] of Object.entries(fields)) root.setFieldValue(value, name); |
| 52 | + root.moveBy(0, y); |
| 53 | + const action = editor.workspace.newBlock("ser_broadcast"); |
| 54 | + const text = editor.workspace.newBlock("ser_text"); |
| 55 | + text.setFieldValue(message, "TEXT"); |
| 56 | + action.getInput("MESSAGE").connection.connect(text.outputConnection); |
| 57 | + const target = editor.workspace.newBlock("ser_target"); |
| 58 | + action.getInput("TARGET").connection.connect(target.outputConnection); |
| 59 | + const duration = editor.workspace.newBlock("ser_duration"); |
| 60 | + action.getInput("DURATION").connection.connect(duration.outputConnection); |
| 61 | + root.getInput("DO").connection.connect(action.previousConnection); |
| 62 | + return root; |
| 63 | +} |
| 64 | + |
| 65 | +function diagnosticsFor(editor) { |
| 66 | + const code = editor.generator.workspaceToCode(editor.workspace).trimEnd(); |
| 67 | + // Copy out of the sandbox so assertions compare plain arrays from this realm. |
| 68 | + return JSON.parse(JSON.stringify(editor.check(code))); |
| 69 | +} |
| 70 | + |
| 71 | +function errorCodes(diagnostics) { |
| 72 | + return diagnostics.filter(item => item.severity === "error").map(item => item.code); |
| 73 | +} |
| 74 | + |
| 75 | +test("blocks a manual start mixed with an event start, in either order", () => { |
| 76 | + // C01: placed first, SER rejects the file; placed last, the manual actions run on the event. |
| 77 | + for (const manualFirst of [true, false]) { |
| 78 | + const editor = createHeadlessEditor(); |
| 79 | + addRoot(editor, "ser_when_manual", manualFirst ? 0 : 400, "manual"); |
| 80 | + addRoot(editor, "ser_when_event", 200, "event", { EVENT: "RoundStarted" }); |
| 81 | + |
| 82 | + const diagnostics = diagnosticsFor(editor); |
| 83 | + assert.deepEqual(errorCodes(diagnostics), ["mixed-manual-entry"]); |
| 84 | + assert.match( |
| 85 | + diagnostics.find(item => item.code === "mixed-manual-entry").message, |
| 86 | + /separate script/ |
| 87 | + ); |
| 88 | + } |
| 89 | +}); |
| 90 | + |
| 91 | +test("blocks a manual start mixed with a command start", () => { |
| 92 | + const editor = createHeadlessEditor(); |
| 93 | + addRoot(editor, "ser_when_command", 0, "command", { COMMAND: "heal" }); |
| 94 | + addRoot(editor, "ser_when_manual", 200, "manual"); |
| 95 | + |
| 96 | + assert.deepEqual(errorCodes(diagnosticsFor(editor)), ["mixed-manual-entry"]); |
| 97 | +}); |
| 98 | + |
| 99 | +test("blocks more than one manual start", () => { |
| 100 | + const editor = createHeadlessEditor(); |
| 101 | + addRoot(editor, "ser_when_manual", 0, "first"); |
| 102 | + addRoot(editor, "ser_when_manual", 200, "second"); |
| 103 | + |
| 104 | + const diagnostics = diagnosticsFor(editor); |
| 105 | + assert.deepEqual(errorCodes(diagnostics), ["multiple-manual-entries"]); |
| 106 | + assert.match( |
| 107 | + diagnostics.find(item => item.code === "multiple-manual-entries").message, |
| 108 | + /separate script/ |
| 109 | + ); |
| 110 | +}); |
| 111 | + |
| 112 | +test("still allows one manual start, and several event or command starts", () => { |
| 113 | + const manual = createHeadlessEditor(); |
| 114 | + addRoot(manual, "ser_when_manual", 0, "manual"); |
| 115 | + assert.deepEqual(errorCodes(diagnosticsFor(manual)), []); |
| 116 | + |
| 117 | + // SER splits a file into one part per event or command start. |
| 118 | + const flagged = createHeadlessEditor(); |
| 119 | + addRoot(flagged, "ser_when_event", 0, "event", { EVENT: "RoundStarted" }); |
| 120 | + addRoot(flagged, "ser_when_command", 200, "command", { COMMAND: "heal" }); |
| 121 | + assert.deepEqual(errorCodes(diagnosticsFor(flagged)), []); |
| 122 | +}); |
0 commit comments