Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
f96d57e11This file is pinned to the selected release. Download the original to open it in your design tool.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { assembleModSource } from './blocks.mjs'
import {
buildModArchive,
detectToolsVersionMismatch,
findFileWithSuffix,
isXsArchive,
xsArchiveVersion,
} from './mod-builder.mjs'
import createTools from './vendor/tools.js'
test('detectToolsVersionMismatch parses the TOOL warning', () => {
const logs = [
'### -p wasm',
'Moddable SDK tools mismatch between binary (8.3.0) and source (9.0.0)! Rebuilding tools.',
]
assert.equal(detectToolsVersionMismatch(logs), '8.3.0')
assert.equal(detectToolsVersionMismatch(['no mismatch here']), null)
})
test('findFileWithSuffix picks the first match', () => {
const paths = ['/a/b.txt', '/a/mc.xsa', '/a/other.xsa']
assert.equal(findFileWithSuffix(paths, '.xsa'), '/a/mc.xsa')
assert.equal(findFileWithSuffix(paths, '.bin'), undefined)
})
test('isXsArchive rejects non-archives', () => {
assert.equal(isXsArchive(new Uint8Array([0, 1, 2])), false)
assert.equal(isXsArchive(new TextEncoder().encode('....XS_A....VERS')), true)
})
test('xsArchiveVersion requires the VERS atom', () => {
// "....XS_A...." + "VERS" + version bytes
const withVers = new TextEncoder().encode('....XS_A....VERS')
const bytes = new Uint8Array(19)
bytes.set(withVers.subarray(0, 16), 0)
bytes[16] = 17
bytes[17] = 8
bytes[18] = 0
assert.deepEqual(xsArchiveVersion(bytes), [17, 8, 0])
// XS_A header but no VERS atom -> null instead of garbage version bytes
const noVers = new TextEncoder().encode('....XS_A....XXXX...')
assert.equal(xsArchiveVersion(noVers), null)
})
test('buildModArchive compiles a mod to a valid XS archive via wasm mcrun', async () => {
const logs = []
const archive = await buildModArchive(createTools, {
modJs: `export async function onContextCreated(robot) {\n trace('hello from test\\n')\n}\n`,
name: 'testmod',
onLog: (line) => logs.push(line),
})
assert.ok(archive instanceof Uint8Array)
assert.ok(archive.length > 100, `archive too small: ${archive.length}`)
assert.ok(isXsArchive(archive), 'archive must start with XS_A atom')
const version = xsArchiveVersion(archive)
assert.ok(version && version[0] >= 17, `unexpected XS version: ${version}`)
const text = logs.join('\n')
assert.match(text, /mcrun/, 'log should include the mcrun invocation')
assert.match(text, /xsa/, 'log should include the xsa archive step')
})
test('buildModArchive compiles generator-style output with host-module imports', async () => {
const source = `import Timer from 'timer'
import { randomBetween, wait } from 'stackchan-util'
import { Emotion } from 'face-state'
function hexToRgb(hex) {
const n = parseInt(hex.slice(1), 16)
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
}
export async function onContextCreated(robot) {
;(async () => {
robot.face.setEmotion(Emotion.HAPPY)
robot.face.setColor('primary', ...hexToRgb('#30e0ff'))
robot.ui.showBalloon(String('こんにちは'))
})().catch((error) => trace('start handler failed: ' + error + '\\n'))
if (robot.input.button?.a) {
robot.input.button.a.onEvent = (event) => {
if (!event.pressed) return
void (async () => {
await robot.audio.say(String('やあ'))
await wait(randomBetween(100, 200))
})().catch((error) => trace('button a handler failed: ' + error + '\\n'))
}
}
Timer.repeat(() => {
robot.motion.lookAt([1, 0.5, 0])
}, 10000)
}
`
const archive = await buildModArchive(createTools, { modJs: source, name: 'generated' })
assert.ok(isXsArchive(archive))
})
test('buildModArchive compiles a MOD using the new event/motion/ui blocks', async () => {
// assembleModSource injects the event dispatch helpers + imports; this checks
// the generated source for the new blocks actually compiles on the device.
const body =
"onButton(robot, 'a', 'release', (event) => {\n void (async () => {\n robot.face.setEmotion(Emotion.HAPPY)\n })().catch((error) => trace('button a handler failed: ' + error + '\\n'))\n})\n" +
"onImu(robot, 'shake', (event) => {\n void (async () => {\n await robot.audio.say(String('わっ'))\n })().catch((error) => trace('imu handler failed: ' + error + '\\n'))\n})\n" +
"onTouchPanel(robot, 'forwardSwipe', (event) => {\n void (async () => {\n robot.ui.toggleDrawer()\n })().catch((error) => trace('touch handler failed: ' + error + '\\n'))\n})\n" +
"robot.ui.drawer?.addDrawerButton({ key: 'k', label: 'ボタン', callback: () => {\n void (async () => {\n robot.ui.showFace()\n })().catch((error) => trace('drawer handler failed: ' + error + '\\n'))\n} })\n" +
'await robot.motion.setPose({ rotation: { p: (30 * Math.PI) / 180, y: (-45 * Math.PI) / 180, r: (0 * Math.PI) / 180 } }, 0.5)\n' +
"robot.lighting.lightBlink('a', ...hexToRgb('#ff4040'), 250)\n"
const source = assembleModSource(body)
const archive = await buildModArchive(createTools, { modJs: source, name: 'newblocks' })
assert.ok(isXsArchive(archive))
})
test('buildModArchive surfaces syntax errors from xsc', async () => {
await assert.rejects(
buildModArchive(createTools, { modJs: 'export function {{{ broken', name: 'brokenmod' }),
(error) => {
assert.match(String(error.message), /build step failed|mcrun failed/)
return true
}
)
})