188 lines
6.3 KiB
JavaScript
188 lines
6.3 KiB
JavaScript
/**
|
||
* 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 };
|
||
}
|