Initial flame_lua_runtime package
This commit is contained in:
593
test/runtime/rendering/render_tree_controller_test.dart
Normal file
593
test/runtime/rendering/render_tree_controller_test.dart
Normal file
@@ -0,0 +1,593 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/game_diff.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_node.dart';
|
||||
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
||||
import 'package:flame_lua_runtime/runtime/rendering/render_tree_controller.dart';
|
||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('RenderTreeController', () {
|
||||
test('creates, updates and removes components by id', () {
|
||||
final events = <RuntimeEvent>[];
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'panel',
|
||||
type: RuntimeNodeType.rect,
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 80,
|
||||
layer: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final created = controller.componentById('panel');
|
||||
expect(created, isNotNull);
|
||||
expect(created!.node.x, 10);
|
||||
expect(created.node.y, 20);
|
||||
expect(created.node.width, 100);
|
||||
expect(created.node.height, 80);
|
||||
expect(created.priority, 4);
|
||||
|
||||
controller.apply(
|
||||
NodeDiff(
|
||||
updates: [
|
||||
NodeUpdate(
|
||||
id: 'panel',
|
||||
props: {'x': 30, 'visible': false, 'layer': 8},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final updated = controller.componentById('panel');
|
||||
expect(updated, same(created));
|
||||
expect(updated!.node.x, 30);
|
||||
expect(updated.node.y, 20);
|
||||
expect(updated.node.visible, isFalse);
|
||||
expect(updated.priority, 8);
|
||||
|
||||
controller.apply(const NodeDiff(removes: [NodeRemove(id: 'panel')]));
|
||||
|
||||
expect(controller.componentById('panel'), isNull);
|
||||
});
|
||||
|
||||
test('replaces an existing component when create uses same id', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [RuntimeNode(id: 'node', type: RuntimeNodeType.rect)],
|
||||
),
|
||||
);
|
||||
final first = controller.componentById('node');
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(id: 'node', type: RuntimeNodeType.circle, layer: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
final second = controller.componentById('node');
|
||||
|
||||
expect(first, isNotNull);
|
||||
expect(second, isNotNull);
|
||||
expect(second, isNot(same(first)));
|
||||
expect(second!.node.type, RuntimeNodeType.circle);
|
||||
expect(second.priority, 2);
|
||||
});
|
||||
|
||||
test('mounts nodes under declared parent and supports reparenting', () {
|
||||
final root = Component();
|
||||
final controller = RenderTreeController(
|
||||
root: root,
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(id: 'panel', type: RuntimeNodeType.panel),
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'panel',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final panel = controller.componentById('panel')!;
|
||||
final button = controller.componentById('button')!;
|
||||
expect(panel.parent, root);
|
||||
expect(button.parent, panel);
|
||||
|
||||
controller.apply(
|
||||
NodeDiff(
|
||||
updates: [
|
||||
NodeUpdate(id: 'button', props: {'parent': ''}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(button.parent, root);
|
||||
expect(button.node.parent, isNull);
|
||||
});
|
||||
|
||||
test('reattaches child when parent is created later', () {
|
||||
final root = Component();
|
||||
final controller = RenderTreeController(
|
||||
root: root,
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'panel',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final button = controller.componentById('button')!;
|
||||
expect(button.parent, root);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [RuntimeNode(id: 'panel', type: RuntimeNodeType.panel)],
|
||||
),
|
||||
);
|
||||
|
||||
expect(button.parent, controller.componentById('panel'));
|
||||
});
|
||||
|
||||
test('scrolls listView by id or point and offsets direct children', () {
|
||||
final root = PositionComponent();
|
||||
final events = <RuntimeEvent>[];
|
||||
final controller = RenderTreeController(
|
||||
root: root,
|
||||
resources: GameResourceManager(),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'list',
|
||||
type: RuntimeNodeType.listView,
|
||||
width: 160,
|
||||
height: 60,
|
||||
contentWidth: 220,
|
||||
contentHeight: 140,
|
||||
scrollX: 10,
|
||||
scrollY: 20,
|
||||
onScroll: 'list_scrolled',
|
||||
),
|
||||
RuntimeNode(
|
||||
id: 'row',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'list',
|
||||
x: 40,
|
||||
y: 50,
|
||||
width: 140,
|
||||
height: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final list = controller.componentById('list')!;
|
||||
final row = controller.componentById('row')!;
|
||||
expect(row.parent, list);
|
||||
expect(row.position, Vector2(30, 30));
|
||||
expect(controller.listViewAt(Vector2(10, 10)), 'list');
|
||||
|
||||
expect(controller.scrollListView('list', deltaX: 30, deltaY: 50), isTrue);
|
||||
expect(controller.componentById('list')!.node.scrollX, 40);
|
||||
expect(controller.componentById('list')!.node.scrollY, 70);
|
||||
expect(row.position, Vector2(0, -20));
|
||||
expect(events.last.type, RuntimeEventType.scroll);
|
||||
expect(events.last.handler, 'list_scrolled');
|
||||
expect(events.last.data['scrollX'], 40);
|
||||
expect(events.last.data['scrollY'], 70);
|
||||
|
||||
expect(
|
||||
controller.scrollListViewAt(Vector2(10, 10), deltaY: 1000),
|
||||
isTrue,
|
||||
);
|
||||
expect(controller.componentById('list')!.node.scrollY, 80);
|
||||
expect(row.position, Vector2(0, -30));
|
||||
expect(
|
||||
controller.scrollListViewAt(Vector2(500, 500), deltaY: 20),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('listView padding offsets children and reduces scroll viewport', () {
|
||||
final root = PositionComponent();
|
||||
final controller = RenderTreeController(
|
||||
root: root,
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'list',
|
||||
type: RuntimeNodeType.listView,
|
||||
width: 120,
|
||||
height: 80,
|
||||
contentWidth: 200,
|
||||
contentHeight: 168,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 12,
|
||||
paddingRight: 8,
|
||||
paddingBottom: 6,
|
||||
scrollbarVisible: false,
|
||||
),
|
||||
RuntimeNode(
|
||||
id: 'row',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'list',
|
||||
x: 4,
|
||||
y: 5,
|
||||
width: 40,
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final row = controller.componentById('row')!;
|
||||
expect(row.position, Vector2(14, 17));
|
||||
expect(
|
||||
controller.scrollListView('list', deltaX: 200, deltaY: 200),
|
||||
isTrue,
|
||||
);
|
||||
expect(controller.componentById('list')!.node.scrollX, 98);
|
||||
expect(controller.componentById('list')!.node.scrollY, 106);
|
||||
expect(row.position, Vector2(-84, -89));
|
||||
});
|
||||
|
||||
test('virtualized listView culls direct children outside cache window', () {
|
||||
final root = PositionComponent();
|
||||
final controller = RenderTreeController(
|
||||
root: root,
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'list',
|
||||
type: RuntimeNodeType.listView,
|
||||
width: 120,
|
||||
height: 60,
|
||||
contentHeight: 400,
|
||||
virtualized: true,
|
||||
cacheExtent: 0,
|
||||
),
|
||||
RuntimeNode(
|
||||
id: 'visible_row',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'list',
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 20,
|
||||
),
|
||||
RuntimeNode(
|
||||
id: 'culled_row',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'list',
|
||||
y: 180,
|
||||
width: 100,
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.componentById('visible_row')!.isVisible, isTrue);
|
||||
expect(controller.componentById('culled_row')!.isVisible, isFalse);
|
||||
|
||||
controller.scrollListView('list', deltaY: 150);
|
||||
|
||||
expect(controller.componentById('visible_row')!.isVisible, isFalse);
|
||||
expect(controller.componentById('culled_row')!.isVisible, isTrue);
|
||||
});
|
||||
|
||||
test('removes descendants when removing parent', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(id: 'panel', type: RuntimeNodeType.panel),
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'panel',
|
||||
),
|
||||
RuntimeNode(
|
||||
id: 'label',
|
||||
type: RuntimeNodeType.text,
|
||||
parent: 'button',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
controller.apply(const NodeDiff(removes: [NodeRemove(id: 'panel')]));
|
||||
|
||||
expect(controller.componentById('panel'), isNull);
|
||||
expect(controller.componentById('button'), isNull);
|
||||
expect(controller.componentById('label'), isNull);
|
||||
});
|
||||
|
||||
test('rejects parent cycles', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(id: 'panel', type: RuntimeNodeType.panel),
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'panel',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => controller.apply(
|
||||
NodeDiff(
|
||||
updates: [
|
||||
NodeUpdate(id: 'panel', props: {'parent': 'button'}),
|
||||
],
|
||||
),
|
||||
),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(controller.componentById('panel')!.node.parent, isNull);
|
||||
});
|
||||
|
||||
test('rejects invalid diff before applying any partial mutation', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(id: 'toast', type: RuntimeNodeType.panel),
|
||||
RuntimeNode(id: 'panel', type: RuntimeNodeType.panel),
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'panel',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => controller.apply(
|
||||
NodeDiff(
|
||||
removes: const [NodeRemove(id: 'toast')],
|
||||
updates: [
|
||||
NodeUpdate(id: 'panel', props: {'parent': 'button'}),
|
||||
],
|
||||
),
|
||||
),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(controller.componentById('toast'), isNotNull);
|
||||
expect(controller.componentById('panel')!.node.parent, isNull);
|
||||
expect(controller.componentById('button')!.node.parent, 'panel');
|
||||
});
|
||||
|
||||
test('clear removes all tracked components', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(id: 'panel', type: RuntimeNodeType.panel),
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
parent: 'panel',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
controller.clear();
|
||||
|
||||
expect(controller.componentById('panel'), isNull);
|
||||
expect(controller.componentById('button'), isNull);
|
||||
});
|
||||
|
||||
test('ignores tap callback from stale replaced component', () {
|
||||
final events = <RuntimeEvent>[];
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
interactive: true,
|
||||
onTap: 'old_tap',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final stale = controller.componentById('button')!;
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
interactive: true,
|
||||
onTap: 'new_tap',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
stale.onNodeTap(stale.node, Vector2(1, 2));
|
||||
controller
|
||||
.componentById('button')!
|
||||
.onNodeTap(controller.componentById('button')!.node, Vector2(3, 4));
|
||||
|
||||
expect(events.map((event) => event.toMap()), [
|
||||
{
|
||||
'type': RuntimeEventType.tap,
|
||||
'target': 'button',
|
||||
'handler': 'new_tap',
|
||||
'x': 3.0,
|
||||
'y': 4.0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('emits tap event from interactive node callback', () {
|
||||
final events = <RuntimeEvent>[];
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [
|
||||
RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
interactive: true,
|
||||
onTap: 'roll_dice',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final component = controller.componentById('button')!;
|
||||
component.onNodeTap(component.node, Vector2(3, 4));
|
||||
|
||||
expect(events, hasLength(1));
|
||||
expect(events.single.toMap(), {
|
||||
'type': RuntimeEventType.tap,
|
||||
'target': 'button',
|
||||
'handler': 'roll_dice',
|
||||
'x': 3.0,
|
||||
'y': 4.0,
|
||||
});
|
||||
expect(events.single.targetEpoch, controller.epochOf('button'));
|
||||
expect(events.single.scopeEpoch, controller.epochOf('button'));
|
||||
});
|
||||
|
||||
test('increments epoch when node is recreated', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [RuntimeNode(id: 'node', type: RuntimeNodeType.rect)],
|
||||
),
|
||||
);
|
||||
final firstEpoch = controller.epochOf('node');
|
||||
|
||||
controller.apply(const NodeDiff(removes: [NodeRemove(id: 'node')]));
|
||||
final removedEpoch = controller.epochOf('node');
|
||||
|
||||
controller.apply(
|
||||
const NodeDiff(
|
||||
creates: [RuntimeNode(id: 'node', type: RuntimeNodeType.rect)],
|
||||
),
|
||||
);
|
||||
final recreatedEpoch = controller.epochOf('node');
|
||||
|
||||
expect(firstEpoch, 1);
|
||||
expect(removedEpoch, greaterThan(firstEpoch));
|
||||
expect(recreatedEpoch, greaterThan(removedEpoch));
|
||||
expect(controller.isNodeEpochAlive('node', firstEpoch), isFalse);
|
||||
expect(controller.isNodeEpochAlive('node', recreatedEpoch), isTrue);
|
||||
});
|
||||
|
||||
test('ignores updates and removes for unknown ids', () {
|
||||
final controller = RenderTreeController(
|
||||
root: Component(),
|
||||
resources: GameResourceManager(),
|
||||
eventSink: (_) {},
|
||||
);
|
||||
|
||||
controller.apply(
|
||||
NodeDiff(
|
||||
updates: [
|
||||
NodeUpdate(id: 'missing', props: {'x': 1}),
|
||||
],
|
||||
removes: const [NodeRemove(id: 'missing')],
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.componentById('missing'), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
165
test/runtime/rendering/runtime_component_test.dart
Normal file
165
test/runtime/rendering/runtime_component_test.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_node.dart';
|
||||
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
||||
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
|
||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('RuntimeComponent', () {
|
||||
test('applies base transform and priority from node', () {
|
||||
final component = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'rect',
|
||||
type: RuntimeNodeType.rect,
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 120,
|
||||
height: 48,
|
||||
scale: 1.5,
|
||||
rotation: 0.25,
|
||||
anchor: RuntimeAnchorValue.center,
|
||||
layer: 7,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
|
||||
expect(component.position, Vector2(10, 20));
|
||||
expect(component.size, Vector2(120, 48));
|
||||
expect(component.scale, Vector2.all(1.5));
|
||||
expect(component.angle, 0.25);
|
||||
expect(component.anchor, Anchor.center);
|
||||
expect(component.priority, 7);
|
||||
expect(component.isVisible, isTrue);
|
||||
});
|
||||
|
||||
test('updates node and transform', () {
|
||||
final component = RuntimeComponent(
|
||||
node: const RuntimeNode(id: 'node', type: RuntimeNodeType.rect),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
|
||||
component.updateNode(
|
||||
const RuntimeNode(
|
||||
id: 'node',
|
||||
type: RuntimeNodeType.progress,
|
||||
x: 30,
|
||||
y: 40,
|
||||
width: 200,
|
||||
height: 16,
|
||||
value: 0.5,
|
||||
layer: 3,
|
||||
),
|
||||
);
|
||||
|
||||
expect(component.node.type, RuntimeNodeType.progress);
|
||||
expect(component.node.value, 0.5);
|
||||
expect(component.position, Vector2(30, 40));
|
||||
expect(component.size, Vector2(200, 16));
|
||||
expect(component.priority, 3);
|
||||
expect(component.isVisible, isTrue);
|
||||
});
|
||||
|
||||
test('visibility hides component subtree and disables hit testing', () {
|
||||
final component = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
text: 'Hidden',
|
||||
width: 100,
|
||||
height: 40,
|
||||
visible: false,
|
||||
interactive: true,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
|
||||
expect(component.isVisible, isFalse);
|
||||
expect(component.containsLocalPoint(Vector2(10, 10)), isFalse);
|
||||
|
||||
component.updateNode(
|
||||
const RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
text: 'Shown',
|
||||
width: 100,
|
||||
height: 40,
|
||||
visible: true,
|
||||
interactive: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(component.isVisible, isTrue);
|
||||
expect(component.containsLocalPoint(Vector2(10, 10)), isTrue);
|
||||
});
|
||||
|
||||
test('supports runtime alpha override for fade commands', () {
|
||||
final component = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'panel',
|
||||
type: RuntimeNodeType.rect,
|
||||
alpha: 0.8,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
|
||||
expect(component.renderAlpha, 0.8);
|
||||
component.setRuntimeAlpha(0.25);
|
||||
expect(component.renderAlpha, 0.25);
|
||||
component.setRuntimeAlpha(2);
|
||||
expect(component.renderAlpha, 1);
|
||||
});
|
||||
|
||||
test('multi-line non-button text is top aligned', () {
|
||||
final component = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'text',
|
||||
type: RuntimeNodeType.text,
|
||||
text: 'line1\nline2',
|
||||
width: 120,
|
||||
height: 80,
|
||||
textAlign: RuntimeTextAlignValue.left,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
|
||||
final text = component.children.whereType<TextComponent>().single;
|
||||
expect(text.anchor, Anchor.topLeft);
|
||||
expect(text.position, Vector2.zero());
|
||||
});
|
||||
|
||||
test('only interactive nodes contain local points', () {
|
||||
final passive = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'passive',
|
||||
type: RuntimeNodeType.rect,
|
||||
width: 100,
|
||||
height: 40,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
final interactive = RuntimeComponent(
|
||||
node: const RuntimeNode(
|
||||
id: 'button',
|
||||
type: RuntimeNodeType.button,
|
||||
width: 100,
|
||||
height: 40,
|
||||
interactive: true,
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
onNodeTap: (_, __) {},
|
||||
);
|
||||
|
||||
expect(passive.containsLocalPoint(Vector2(10, 10)), isFalse);
|
||||
expect(interactive.containsLocalPoint(Vector2(10, 10)), isTrue);
|
||||
expect(interactive.containsLocalPoint(Vector2(101, 10)), isFalse);
|
||||
expect(interactive.containsLocalPoint(Vector2(10, 41)), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
98
test/runtime/rendering/runtime_viewport_test.dart
Normal file
98
test/runtime/rendering/runtime_viewport_test.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_lua_runtime/runtime/display/runtime_viewport.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('RuntimeViewport', () {
|
||||
test('fits design size inside screen with letterboxing', () {
|
||||
final transform = RuntimeViewport.compute(
|
||||
screenSize: Vector2(1280, 720),
|
||||
config: const RuntimeViewportConfig(
|
||||
designWidth: 720,
|
||||
designHeight: 720,
|
||||
),
|
||||
);
|
||||
|
||||
expect(transform.x, 280);
|
||||
expect(transform.y, 0);
|
||||
expect(transform.width, 720);
|
||||
expect(transform.height, 720);
|
||||
expect(transform.scaleX, 1);
|
||||
expect(transform.scaleY, 1);
|
||||
});
|
||||
|
||||
test('fills screen by preserving aspect ratio and cropping', () {
|
||||
final transform = RuntimeViewport.compute(
|
||||
screenSize: Vector2(1280, 720),
|
||||
config: const RuntimeViewportConfig(
|
||||
designWidth: 720,
|
||||
designHeight: 720,
|
||||
scaleMode: RuntimeScaleMode.fill,
|
||||
),
|
||||
);
|
||||
|
||||
expect(transform.x, 0);
|
||||
expect(transform.y, -280);
|
||||
expect(transform.width, 1280);
|
||||
expect(transform.height, 1280);
|
||||
expect(transform.scaleX, closeTo(1.777777, 0.00001));
|
||||
expect(transform.scaleY, closeTo(1.777777, 0.00001));
|
||||
});
|
||||
|
||||
test('stretches design independently on both axes', () {
|
||||
final transform = RuntimeViewport.compute(
|
||||
screenSize: Vector2(1440, 720),
|
||||
config: const RuntimeViewportConfig(
|
||||
designWidth: 720,
|
||||
designHeight: 720,
|
||||
scaleMode: RuntimeScaleMode.stretch,
|
||||
),
|
||||
);
|
||||
|
||||
expect(transform.x, 0);
|
||||
expect(transform.y, 0);
|
||||
expect(transform.width, 1440);
|
||||
expect(transform.height, 720);
|
||||
expect(transform.scaleX, 2);
|
||||
expect(transform.scaleY, 1);
|
||||
});
|
||||
|
||||
test('centers design without scaling', () {
|
||||
final transform = RuntimeViewport.compute(
|
||||
screenSize: Vector2(1000, 800),
|
||||
config: const RuntimeViewportConfig(
|
||||
designWidth: 720,
|
||||
designHeight: 720,
|
||||
scaleMode: RuntimeScaleMode.none,
|
||||
),
|
||||
);
|
||||
|
||||
expect(transform.x, 140);
|
||||
expect(transform.y, 40);
|
||||
expect(transform.width, 720);
|
||||
expect(transform.height, 720);
|
||||
expect(transform.scaleX, 1);
|
||||
expect(transform.scaleY, 1);
|
||||
});
|
||||
|
||||
test('applies transform to root component', () {
|
||||
final root = PositionComponent();
|
||||
RuntimeViewport.apply(
|
||||
root,
|
||||
const RuntimeViewportTransform(
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 300,
|
||||
height: 400,
|
||||
scaleX: 2,
|
||||
scaleY: 3,
|
||||
scaleMode: RuntimeScaleMode.stretch,
|
||||
),
|
||||
);
|
||||
|
||||
expect(root.position, Vector2(10, 20));
|
||||
expect(root.size, Vector2(300, 400));
|
||||
expect(root.scale, Vector2(2, 3));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user