Initial flame_lua_runtime package
This commit is contained in:
962
test/runtime/scripting/lua_dardo_script_engine_test.dart
Normal file
962
test/runtime/scripting/lua_dardo_script_engine_test.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user