/** * rbxl-lua-mvp.test.js — headless smoke-тест Roblox Lua API shim. * * НЕ запускает Worker (это требует браузерного Worker API). Вместо этого * напрямую импортирует roblox-shim.js и инициализирует Lua в текущем потоке. * * Запуск: node --experimental-vm-modules tests/rbxl-lua-mvp.test.js */ import { LuaFactory } from 'wasmoon'; import { registerRobloxApi } from '../src/engine/roblox-shim.js'; const FAKE_SCENE_SNAP = { primitives: { 1: { id: 1, type: 'cube', name: 'Floor', x: 0, y: 0, z: 0, sx: 10, sy: 1, sz: 10, color: '#888888', material: 'glossy', anchored: true, canCollide: true, opacity: 1 }, 2: { id: 2, type: 'cube', name: 'KillBrick', x: 5, y: 1, z: 0, sx: 2, sy: 1, sz: 2, color: '#ff0000', material: 'neon', anchored: true, canCollide: true, opacity: 1 }, }, }; const SNIPPETS = [ { name: 'print hello', lua: `print("Hello from Lua!")`, expectLogs: [{ level: 'info', text: 'Hello from Lua!' }], }, { name: 'Vector3 math', lua: ` local v = Vector3.new(3, 4, 0) print("magnitude:", v.Magnitude) local u = v.Unit print("unit:", u.X, u.Y, u.Z) `, expectLogs: [ { level: 'info', text: 'magnitude:\t5' }, ], }, { name: 'workspace iteration', lua: ` local children = workspace:GetChildren() print("count:", #children) for i, c in ipairs(children) do print("child:", c.Name, "class:", c.ClassName) end `, expectLogs: [ { level: 'info', text: 'count:\t2' }, ], }, { name: 'FindFirstChild', lua: ` local kb = workspace:FindFirstChild("KillBrick") if kb then print("found:", kb.Name) else print("not found") end `, expectLogs: [{ level: 'info', text: 'found:\tKillBrick' }], }, { name: 'Part.Position get', lua: ` local kb = workspace:FindFirstChild("KillBrick") print("position:", kb.Position.X, kb.Position.Y, kb.Position.Z) `, expectLogs: [{ level: 'info', text: 'position:\t5\t1\t0' }], }, { name: 'Part.Color set', lua: ` local kb = workspace:FindFirstChild("KillBrick") kb.Color = Color3.new(0, 1, 0) print("new color hex (via Position):", kb.Color.R, kb.Color.G, kb.Color.B) `, expectPartSet: { primId: 2, prop: 'color' }, }, { name: 'CFrame.Angles', lua: ` local cf = CFrame.Angles(0, math.pi/2, 0) print("lookvector:", cf.LookVector.X, cf.LookVector.Y, cf.LookVector.Z) `, expectLogs: [], }, { name: 'Instance.new + Parent', lua: ` local f = Instance.new("Folder", workspace) f.Name = "MyFolder" print("folder name:", f.Name, "parent:", f.Parent.Name) `, expectLogs: [{ level: 'info', text: 'folder name:\tMyFolder\tparent:\tWorkspace' }], }, { name: 'IsA hierarchy', lua: ` local kb = workspace:FindFirstChild("KillBrick") print("isa Part:", kb:IsA("Part")) print("isa BasePart:", kb:IsA("BasePart")) print("isa Instance:", kb:IsA("Instance")) print("isa Sound:", kb:IsA("Sound")) `, expectLogs: [ { level: 'info', text: 'isa Part:\ttrue' }, { level: 'info', text: 'isa BasePart:\ttrue' }, { level: 'info', text: 'isa Instance:\ttrue' }, { level: 'info', text: 'isa Sound:\tfalse' }, ], }, ]; async function runSnippet(snippet) { const factory = new LuaFactory(); const lua = await factory.createEngine(); const logs = []; const sent = []; const send = (cmd, payload) => sent.push({ cmd, payload }); registerRobloxApi(lua, { getSceneSnap: () => FAKE_SCENE_SNAP, targetPrimitiveId: 2, // как будто скрипт прикреплён к KillBrick send, }); // Перехват print через send('log', ...) let errMsg = null; try { await lua.doString(snippet.lua); } catch (e) { errMsg = e && e.message ? e.message : String(e); } lua.global.close(); const captured = sent.filter(s => s.cmd === 'log'); return { logs: captured.map(s => s.payload), partSets: sent.filter(s => s.cmd === 'partSet'), error: errMsg }; } (async () => { let passed = 0; let failed = 0; for (const s of SNIPPETS) { const result = await runSnippet(s); const ok = checkExpectations(s, result); if (ok.success) { console.log(`✓ ${s.name}`); passed++; } else { console.log(`✗ ${s.name}`); console.log(` error: ${result.error || 'none'}`); console.log(` logs received:`); for (const l of result.logs) console.log(` [${l.level}] ${JSON.stringify(l.text)}`); if (result.partSets.length) { console.log(` partSets:`, JSON.stringify(result.partSets)); } console.log(` reason: ${ok.reason}`); failed++; } } console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed > 0 ? 1 : 0); })(); function checkExpectations(snippet, result) { if (result.error) { return { success: false, reason: `lua error: ${result.error}` }; } if (snippet.expectLogs) { for (const exp of snippet.expectLogs) { const found = result.logs.find(l => l.level === exp.level && l.text === exp.text); if (!found) { return { success: false, reason: `missing log: [${exp.level}] ${JSON.stringify(exp.text)}` }; } } } if (snippet.expectPartSet) { const found = result.partSets.find(s => s.payload.primId === snippet.expectPartSet.primId && s.payload.prop === snippet.expectPartSet.prop ); if (!found) { return { success: false, reason: `missing partSet ${JSON.stringify(snippet.expectPartSet)}` }; } } return { success: true }; }