Initial flame_lua_runtime package

This commit is contained in:
gem
2026-06-07 22:53:58 +08:00
commit 733b2fb798
262 changed files with 28439 additions and 0 deletions

View File

@@ -0,0 +1,962 @@
import 'dart:io';
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
import 'package:flame_lua_runtime/runtime/scripting/lua_dardo_script_engine.dart';
import 'package:flutter_test/flutter_test.dart';
Future<GamePackage> _loadExamplePackage(String gameId) async {
final root = 'example/assets/games/$gameId';
final manifest = await File('$root/manifest.json').readAsString();
return GamePackage.file(
rootPath: root,
manifest: GamePackageManifest.fromJsonString(manifest),
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('LuaDardoScriptEngine runtime.import', () {
test('loads bundled Ludo module graph', () async {
final package = await _loadExamplePackage('ludo');
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({'runtimeApiVersion': 1});
expect(diff.render.creates, isNotEmpty);
expect(diff.ui.creates, isNotEmpty);
expect(
diff.render.creates.map((node) => node.id),
contains('board_panel'),
);
expect(diff.ui.creates.map((node) => node.id), contains('dice_button'));
final diceButton = diff.ui.creates.singleWhere(
(node) => node.id == 'dice_button',
);
expect(diceButton.parent, 'top_bar');
expect(diceButton.x, 540);
expect(diceButton.y, 14);
expect(diceButton.onTap, 'roll_dice');
final eventDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'dice_button',
handler: 'roll_dice',
),
);
final sound = eventDiff.commands.single;
expect(sound.type, RuntimeCommandType.playSound);
expect(sound.payload['asset'], 'dice');
});
test('loads bundled Ludo with English localization context', () async {
final package = await _loadExamplePackage('ludo');
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
final context = {
'runtimeApiVersion': 1,
'locale': {
'requested': 'en-US',
'resolved': 'en',
'default': 'zh-Hans',
'supported': ['zh-Hans', 'en'],
'languageCode': 'en',
'countryCode': 'US',
},
};
expect(engine.smokeTest(context), isTrue);
final diff = engine.init(context);
expect(
diff.render.creates
.singleWhere((node) => node.id == 'board_title')
.text,
'Lua Ludo',
);
expect(
diff.ui.creates.singleWhere((node) => node.id == 'dice_button').text,
'Roll',
);
expect(
diff.ui.creates.singleWhere((node) => node.id == 'turn_text').text,
'Current player: Red',
);
});
test('loads bundled Flight module graph and basic dice flow', () async {
final package = await _loadExamplePackage('flight');
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({'runtimeApiVersion': 1});
expect(
diff.render.creates.map((node) => node.id),
contains('board_panel'),
);
expect(diff.render.creates.map((node) => node.id), contains('red_1'));
expect(diff.ui.creates.map((node) => node.id), contains('dice_button'));
final rollDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'dice_button',
handler: 'roll_dice',
),
);
expect(
rollDiff.render.updates.map((update) => update.id),
contains('red_1'),
);
final moveDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'red_1',
handler: 'piece_tap',
),
);
expect(moveDiff.commands.single.type, RuntimeCommandType.movePath);
expect(moveDiff.commands.single.target, 'red_1');
});
test(
'loads bundled Template package as minimal integration starter',
() async {
final package = await _loadExamplePackage('template');
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({
'runtimeApiVersion': 1,
'gameId': 'template',
});
expect(
diff.render.creates.map((node) => node.id),
contains('template_bg'),
);
expect(
diff.render.creates.map((node) => node.id),
contains('template_start'),
);
final tapDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'template_start',
handler: 'template_start',
),
);
expect(
tapDiff.ui.updates.map((update) => update.id),
containsAll([
'template_start',
'template_counter',
'template_status',
]),
);
expect(tapDiff.commands.single.type, RuntimeCommandType.toast);
},
);
test('loads bundled Showcase module graph and command examples', () async {
final package = await _loadExamplePackage('showcase');
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({'runtimeApiVersion': 1});
final nodeIds = diff.render.creates.map((node) => node.id);
expect(nodeIds, contains('example_list_panel'));
expect(nodeIds, contains('example_nodes'));
expect(nodeIds, contains('example_text_demo'));
expect(nodeIds, contains('example_buttons'));
expect(nodeIds, contains('example_button_images'));
expect(nodeIds, contains('example_sprites'));
expect(nodeIds, contains('example_radio_group'));
expect(nodeIds, contains('example_list_view'));
expect(nodeIds, contains('example_layout_demo'));
expect(nodeIds, contains('example_commands'));
expect(nodeIds, contains('example_i18n'));
expect(nodeIds, contains('example_responsive'));
expect(nodeIds, contains('detail_panel'));
expect(nodeIds, contains('detail_action_1'));
expect(nodeIds, contains('sample_rect'));
expect(nodeIds, contains('sample_circle'));
expect(nodeIds, contains('sample_image_node'));
expect(nodeIds, contains('sample_sprite_node'));
expect(nodeIds, contains('sample_progress'));
expect(nodeIds, contains('text_plain_title'));
expect(nodeIds, contains('button_primary'));
expect(nodeIds, contains('image_button_normal'));
expect(nodeIds, contains('image_button_toggle'));
expect(nodeIds, contains('image_button_disabled'));
expect(nodeIds, contains('sprite_sprite_demo'));
expect(nodeIds, contains('radio_value_text'));
expect(nodeIds, contains('list_row_1'));
expect(nodeIds, contains('particle_burst'));
expect(nodeIds, contains('particle_trail'));
expect(nodeIds, contains('particle_snow'));
expect(nodeIds, contains('layout_chip_1'));
expect(nodeIds, contains('layout_chip_4'));
final imageButton = diff.render.creates.singleWhere(
(node) => node.id == 'image_button_normal',
);
expect(imageButton.asset, 'button_normal');
expect(imageButton.pressedAsset, 'button_pressed');
expect(imageButton.disabledAsset, 'button_disabled');
final disabledImageButton = diff.render.creates.singleWhere(
(node) => node.id == 'image_button_disabled',
);
expect(disabledImageButton.interactive, isFalse);
expect(diff.commands, isEmpty);
final imageButtonSelectDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_button_images',
handler: 'select_example',
),
);
expect(
imageButtonSelectDiff.ui.updates.map((update) => update.id),
containsAll(['detail_title', 'detail_code', 'image_button_normal']),
);
final imageButtonToggleDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_2',
handler: 'demo_button_image_toggle',
),
);
final toggleUpdate = imageButtonToggleDiff.ui.updates.singleWhere(
(update) => update.id == 'image_button_toggle',
);
expect(toggleUpdate.props, containsPair('interactive', false));
final selectDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_commands',
handler: 'select_example',
),
);
expect(
selectDiff.ui.updates.map((update) => update.id),
containsAll(['detail_title', 'detail_code', 'detail_action_1']),
);
final paramsDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_tab_params',
handler: 'detail_tab_params',
),
);
final detailCodeUpdate = paramsDiff.ui.updates.firstWhere(
(update) => update.id == 'detail_code',
);
final paramsText = detailCodeUpdate.props['text'];
expect(paramsText, contains('参数说明'));
expect(
paramsDiff.ui.updates
.firstWhere(
(update) =>
update.id == 'detail_code' &&
update.props['textAlign'] != null,
)
.props['textAlign'],
'left',
);
expect(
paramsDiff.ui.updates
.firstWhere((update) => update.id == 'detail_tab_params')
.props['onTap'],
'detail_tab_params',
);
final listPickDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'list_row_3',
handler: 'demo_list_pick_3',
),
);
expect(
listPickDiff.ui.updates
.firstWhere((update) => update.id == 'list_panel')
.props['scrollX'],
0,
);
expect(
listPickDiff.ui.updates
.firstWhere((update) => update.id == 'list_row_3')
.props['x'],
8,
);
expect(
listPickDiff.ui.updates
.firstWhere((update) => update.id == 'list_row_3_text')
.props['textAlign'],
'left',
);
final selectLayoutDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_layout_demo',
handler: 'select_example',
),
);
expect(
selectLayoutDiff.ui.updates
.firstWhere((update) => update.id == 'detail_action_3')
.props['onTap'],
'demo_layout_box',
);
final boxLayoutDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_3',
handler: 'demo_layout_box',
),
);
expect(
boxLayoutDiff.ui.updates
.firstWhere((update) => update.id == 'layout_chip_3')
.props,
containsPair('y', 90),
);
expect(
boxLayoutDiff.ui.updates
.firstWhere((update) => update.id == 'layout_label')
.props['text'],
contains('2 行 × 2 列'),
);
engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_particles',
handler: 'select_example',
),
);
final particleParamsDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_tab_params',
handler: 'detail_tab_params',
),
);
expect(
particleParamsDiff.ui.updates
.firstWhere(
(update) =>
update.id == 'detail_code' && update.props['height'] != null,
)
.props['height'],
greaterThan(400),
);
expect(
particleParamsDiff.ui.updates
.firstWhere((update) => update.id == 'code_panel')
.props['contentHeight'],
greaterThan(450),
);
final animDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_1',
handler: 'demo_anim',
),
);
expect(animDiff.commands.single.type, RuntimeCommandType.sequence);
expect(animDiff.commands.single.payload['commands'], isNotEmpty);
final dialogDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_1',
handler: 'demo_dialog',
),
);
expect(
dialogDiff.ui.creates.map((node) => node.id),
contains('sample_dialog'),
);
final soundDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_1',
handler: 'demo_sound',
),
);
expect(soundDiff.commands.single.type, RuntimeCommandType.playSound);
expect(soundDiff.commands.single.payload['asset'], 'click');
final textSelectDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_text_demo',
handler: 'select_example',
),
);
expect(
textSelectDiff.ui.updates.map((update) => update.id),
containsAll(['text_plain_title', 'text_rich_note']),
);
final textStyleDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_2',
handler: 'demo_text_style',
),
);
expect(
textStyleDiff.ui.updates.map((update) => update.id),
containsAll(['text_plain_title', 'text_style_badge']),
);
final buttonSelectDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_buttons',
handler: 'select_example',
),
);
expect(
buttonSelectDiff.ui.updates.map((update) => update.id),
containsAll(['button_primary', 'button_state_text']),
);
final buttonToggleDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_2',
handler: 'demo_button_toggle',
),
);
expect(
buttonToggleDiff.ui.updates.map((update) => update.id),
containsAll(['button_primary', 'button_state_text']),
);
final spriteDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_1',
handler: 'demo_sprite_anim',
),
);
expect(spriteDiff.commands.single.type, RuntimeCommandType.parallel);
final radioSelectDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_radio_group',
handler: 'select_example',
),
);
expect(
radioSelectDiff.ui.updates.map((update) => update.id),
containsAll(['radio_audio_dot', 'radio_value_text']),
);
final radioDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_2',
handler: 'demo_radio_spine',
),
);
expect(
radioDiff.ui.updates.map((update) => update.id),
containsAll(['radio_spine_dot', 'radio_value_text']),
);
final listSelectDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_list_view',
handler: 'select_example',
),
);
expect(
listSelectDiff.ui.updates.map((update) => update.id),
containsAll(['list_row_1', 'list_value_text']),
);
final listDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_2',
handler: 'demo_list_next',
),
);
expect(
listDiff.ui.updates.map((update) => update.id),
containsAll(['list_row_1', 'list_row_2', 'list_value_text']),
);
final layoutDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_2',
handler: 'demo_layout_column',
),
);
expect(
layoutDiff.ui.updates.map((update) => update.id),
containsAll(['layout_chip_1', 'layout_chip_2', 'layout_label']),
);
final i18nDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'example_i18n',
handler: 'select_example',
),
);
expect(
i18nDiff.ui.updates
.singleWhere((update) => update.id == 'detail_title')
.props['text'],
'Lua 多语言 Showcase',
);
final localeDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_1',
handler: 'demo_i18n_toggle',
),
);
expect(
localeDiff.ui.updates
.singleWhere((update) => update.id == 'detail_title')
.props['text'],
'Lua-owned localization',
);
expect(localeDiff.commands.single.type, RuntimeCommandType.toast);
final responsiveDiff = engine.dispatchEvent(
const RuntimeEvent(
type: RuntimeEventType.tap,
target: 'detail_action_1',
handler: 'demo_responsive_phone',
),
);
expect(
responsiveDiff.ui.updates.map((update) => update.id),
containsAll(['responsive_info', 'responsive_device']),
);
});
test(
'loads manifest-declared modules and caches returned values',
() async {
final package = await _createPackage(
mainScript: '''
local theme_a = runtime.import("theme")
local theme_b = runtime.import("theme")
function smoke_test(ctx)
return theme_a == theme_b and load_count == 1
end
function init(ctx)
return {
ui = {
creates = {
{ id = "title", type = "text", text = theme_a.title }
}
}
}
end
function on_event(event)
return {}
end
''',
modules: {
'theme': '''
load_count = (load_count or 0) + 1
return { title = "Imported Theme" }
''',
},
);
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({'runtimeApiVersion': 1});
expect(diff.ui.creates.single.text, 'Imported Theme');
},
);
test('runtime_ui supports table-style component options', () async {
final runtimeUi = File(
'assets/runtime/lua/runtime_ui.lua',
).readAsStringSync();
final package = await _createPackage(
mainScript: '''
local ui = runtime.import("runtime_ui")
function smoke_test(ctx)
return ui.rect ~= nil and ui.button ~= nil
end
function init(ctx)
return {
ui = {
creates = {
ui.rect("card", { x = 10, y = 20, w = 120, h = 44, radius = 8 }),
ui.circle("dot", { x = 5, y = 6, size = 18, color = "#ffffffff" }),
ui.button("ok", { text = "OK", x = 1, y = 2, w = 60, h = 24, handler = "submit", asset = "button_normal", pressedAsset = "button_pressed", disabledAsset = "button_disabled" })
},
updates = {
ui.update("card", { w = 140, h = 48, onClick = "tap_card" })
}
}
}
end
function on_event(event)
return {}
end
''',
modules: {'runtime_ui': runtimeUi},
);
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({'runtimeApiVersion': 1});
final card = diff.ui.creates.singleWhere((node) => node.id == 'card');
expect(card.width, 120);
expect(card.height, 44);
final dot = diff.ui.creates.singleWhere((node) => node.id == 'dot');
expect(dot.width, 18);
expect(dot.height, 18);
final button = diff.ui.creates.singleWhere((node) => node.id == 'ok');
expect(button.onTap, 'submit');
expect(button.width, 60);
expect(button.asset, 'button_normal');
expect(button.pressedAsset, 'button_pressed');
expect(button.disabledAsset, 'button_disabled');
final update = diff.ui.updates.single;
expect(update.props, containsPair('width', 140));
expect(update.props, containsPair('height', 48));
expect(update.props, containsPair('onTap', 'tap_card'));
expect(update.props.containsKey('w'), isFalse);
expect(update.props.containsKey('h'), isFalse);
expect(update.props.containsKey('onClick'), isFalse);
});
test('runtime_widgets exposes common lightweight components', () async {
final runtimeUi = File(
'assets/runtime/lua/runtime_ui.lua',
).readAsStringSync();
final runtimeWidgets = File(
'assets/runtime/lua/runtime_widgets.lua',
).readAsStringSync();
final package = await _createPackage(
mainScript: '''
local widgets = runtime.import("runtime_widgets")
widgets.configure({
primary = "#ff010203",
secondary = "#ff040506",
surface = "#ff070809",
surfaceAlt = "#ff0a0b0c",
text = "#ff0d0e0f",
muted = "#ff101112",
transparent = "#00000000"
})
function smoke_test(ctx)
return widgets.label ~= nil and widgets.pill ~= nil and widgets.text_button ~= nil and widgets.tabs ~= nil and widgets.list_item ~= nil and widgets.action_row ~= nil and widgets.panel_header ~= nil
end
function init(ctx)
local pill = widgets.pill("status", {
text = "Ready",
x = 10,
y = 20,
w = 72,
h = 24,
color = "#ff000000",
textStyle = { color = "#ffffffff" }
})
local tabs = widgets.tabs("tabs", {
{ key = "code", text = "Code", handler = "show_code" },
{ key = "params", text = "Params", handler = "show_params" }
}, {
x = 8,
y = 86,
itemWidth = 70,
itemHeight = 22,
selected = "params",
parent = "toolbar",
layer = 9
})
local actions = widgets.action_row("actions", {
{ text = "Run", handler = "run" },
{ text = "Stop", handler = "stop", visible = false }
}, {
x = 10,
y = 142,
width = 170,
itemHeight = 24,
gap = 10,
layer = 7
})
local header = widgets.panel_header("header", {
eyebrow = "Runtime",
title = "Panel",
summary = "Composable header",
x = 6,
y = 172,
w = 180,
parent = "card",
layer = 6
})
return {
ui = {
creates = {
widgets.section_title("title", { text = "Overview", x = 4, y = 6, w = 160, h = 28 }),
pill[1],
pill[2],
widgets.text_button("more", { text = "More", x = 10, y = 52, w = 80, h = 28, handler = "open", variant = "ghost" }),
widgets.list_item("row", { text = "Row", x = 10, y = 112, w = 100, h = 24, handler = "select_row", selected = true }),
tabs[1],
tabs[2],
actions[1],
actions[2],
header[1],
header[2],
header[3]
}
}
}
end
function on_event(event)
return {}
end
''',
modules: {'runtime_ui': runtimeUi, 'runtime_widgets': runtimeWidgets},
);
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
expect(engine.smokeTest({'runtimeApiVersion': 1}), isTrue);
final diff = engine.init({'runtimeApiVersion': 1});
expect(
diff.ui.creates.map((node) => node.id),
containsAll([
'title',
'status',
'status_text',
'more',
'row',
'tabs_code',
'tabs_params',
'actions_1',
'actions_2',
'header_eyebrow',
'header_title',
'header_summary',
]),
);
final title = diff.ui.creates.singleWhere((node) => node.id == 'title');
expect(title.fontSize, 18);
final pillText = diff.ui.creates.singleWhere(
(node) => node.id == 'status_text',
);
expect(pillText.parent, 'status');
final more = diff.ui.creates.singleWhere((node) => node.id == 'more');
expect(more.onTap, 'open');
expect(more.color?.toARGB32(), 0x00000000);
final row = diff.ui.creates.singleWhere((node) => node.id == 'row');
expect(row.onTap, 'select_row');
expect(row.color?.toARGB32(), 0xff010203);
final codeTab = diff.ui.creates.singleWhere(
(node) => node.id == 'tabs_code',
);
expect(codeTab.parent, 'toolbar');
expect(codeTab.x, 8);
expect(codeTab.y, 86);
expect(codeTab.onTap, 'show_code');
expect(codeTab.color?.toARGB32(), 0xff070809);
final paramsTab = diff.ui.creates.singleWhere(
(node) => node.id == 'tabs_params',
);
expect(paramsTab.x, 84);
expect(paramsTab.onTap, 'show_params');
expect(paramsTab.color?.toARGB32(), 0xff010203);
final action1 = diff.ui.creates.singleWhere(
(node) => node.id == 'actions_1',
);
final action2 = diff.ui.creates.singleWhere(
(node) => node.id == 'actions_2',
);
expect(action1.width, 80);
expect(action1.onTap, 'run');
expect(action1.layer, 7);
expect(action2.visible, isFalse);
expect(action2.onTap, 'noop');
final eyebrow = diff.ui.creates.singleWhere(
(node) => node.id == 'header_eyebrow',
);
final headerTitle = diff.ui.creates.singleWhere(
(node) => node.id == 'header_title',
);
final summary = diff.ui.creates.singleWhere(
(node) => node.id == 'header_summary',
);
expect(eyebrow.parent, 'card');
expect(eyebrow.y, 172);
expect(headerTitle.y, 196);
expect(summary.y, 228);
});
test('layout supports spacing and grid aliases', () async {
final runtimeUi = File(
'assets/runtime/lua/runtime_ui.lua',
).readAsStringSync();
final layout = File('assets/runtime/lua/layout.lua').readAsStringSync();
final package = await _createPackage(
mainScript: '''
local ui = runtime.import("runtime_ui")
local layout = runtime.import("layout")
function smoke_test(ctx)
return layout.box ~= nil and layout.item ~= nil
end
function init(ctx)
local nodes = layout.box("panel", {
layout.item(ui.rect("a", { w = 20, h = 10 }), { mx = 2, my = 1 }),
ui.rect("b", { w = 20, h = 10 }),
ui.rect("c", { w = 20, h = 10 })
}, {
x = 10,
y = 20,
cols = 2,
cellW = 30,
cellH = 20,
gap = 5,
padding = 3,
align = "center",
valign = "center"
})
return { ui = { creates = nodes } }
end
function on_event(event)
return {}
end
''',
modules: {'runtime_ui': runtimeUi, 'layout': layout},
);
final engine = LuaDardoScriptEngine();
await engine.loadPackage(package);
final diff = engine.init({'runtimeApiVersion': 1});
final a = diff.ui.creates.singleWhere((node) => node.id == 'a');
final b = diff.ui.creates.singleWhere((node) => node.id == 'b');
final c = diff.ui.creates.singleWhere((node) => node.id == 'c');
expect(a.parent, 'panel');
expect(a.x, 18);
expect(a.y, 28);
expect(b.x, 53);
expect(b.y, 28);
expect(c.x, 18);
expect(c.y, 53);
});
test('rejects undeclared module imports', () async {
final package = await _createPackage(
mainScript: '''
runtime.import("missing")
function smoke_test(ctx) return true end
function init(ctx) return {} end
function on_event(event) return {} end
''',
);
final engine = LuaDardoScriptEngine();
await expectLater(engine.loadPackage(package), throwsStateError);
});
test('rejects unsafe module names', () async {
final package = await _createPackage(
mainScript: '''
runtime.import("../theme")
function smoke_test(ctx) return true end
function init(ctx) return {} end
function on_event(event) return {} end
''',
modules: {'theme': 'return {}'},
);
final engine = LuaDardoScriptEngine();
await expectLater(engine.loadPackage(package), throwsStateError);
});
});
}
Future<GamePackage> _createPackage({
required String mainScript,
Map<String, String> modules = const {},
}) async {
final root = await Directory.systemTemp.createTemp('lua_engine_test_');
Directory('${root.path}/scripts').createSync(recursive: true);
File('${root.path}/scripts/main.lua').writeAsStringSync(mainScript);
final manifestModules = <String, String>{};
for (final entry in modules.entries) {
final path = 'scripts/${entry.key}.lua';
File('${root.path}/$path').writeAsStringSync(entry.value);
manifestModules[entry.key] = path;
}
addTearDown(() {
if (root.existsSync()) {
root.deleteSync(recursive: true);
}
});
return GamePackage.file(
rootPath: root.path,
manifest: GamePackageManifest(
gameId: 'test',
name: 'Test',
version: '0.1.0',
runtimeApiVersion: 1,
entry: 'scripts/main.lua',
assetsBase: 'assets',
modules: manifestModules,
),
);
}