Files
flutter_lua_runtime/test/runtime/rendering/runtime_component_test.dart
2026-06-09 12:06:58 +08:00

260 lines
7.9 KiB
Dart

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/material.dart' show Color;
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('multiplies color alpha with node and runtime alpha', () {
expect(
composeRuntimeColorAlpha(const Color(0xffffffff), 1).a,
closeTo(1, 0.001),
);
expect(
composeRuntimeColorAlpha(const Color(0x80ffffff), 1).a,
closeTo(0.5, 0.003),
);
expect(
composeRuntimeColorAlpha(const Color(0x80ffffff), 0.5).a,
closeTo(0.25, 0.003),
);
expect(
composeRuntimeColorAlpha(const Color(0x00ffffff), 1).a,
closeTo(0, 0.001),
);
expect(
composeRuntimeColorAlpha(const Color(0x80ffffff), 0.25).a,
closeTo(0.125, 0.003),
);
});
test('updates text alpha style without rebuilding text component', () {
final component = RuntimeComponent(
node: const RuntimeNode(
id: 'text',
type: RuntimeNodeType.text,
text: 'Fade me',
alpha: 0,
color: Color(0xffffffff),
),
resources: GameResourceManager(),
onNodeTap: (_, __) {},
);
final text = component.children.whereType<TextComponent>().single;
expect(((text.textRenderer as TextPaint).style.color!).a, 0);
component.setRuntimeAlpha(1);
final updatedText = component.children.whereType<TextComponent>().single;
expect(identical(updatedText, text), isTrue);
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
});
test('updates button text alpha style without rebuilding text component', () {
final component = RuntimeComponent(
node: const RuntimeNode(
id: 'button',
type: RuntimeNodeType.button,
text: 'Fade me',
alpha: 0,
),
resources: GameResourceManager(),
onNodeTap: (_, __) {},
);
final text = component.children.whereType<TextComponent>().single;
expect(((text.textRenderer as TextPaint).style.color!).a, 0);
component.setRuntimeAlpha(1);
final updatedText = component.children.whereType<TextComponent>().single;
expect(identical(updatedText, text), isTrue);
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
});
test('applies text shadow style', () {
final component = RuntimeComponent(
node: const RuntimeNode(
id: 'text',
type: RuntimeNodeType.text,
text: 'Shadowed',
alpha: 0.5,
textShadowColor: Color(0x80000000),
textShadowOffsetX: 2,
textShadowOffsetY: 3,
textShadowBlur: 4,
),
resources: GameResourceManager(),
onNodeTap: (_, __) {},
);
final text = component.children.whereType<TextComponent>().single;
final style = (text.textRenderer as TextPaint).style;
final shadow = style.shadows!.single;
expect(shadow.color.a, closeTo(0.25, 0.003));
expect(shadow.offset.dx, 2);
expect(shadow.offset.dy, 3);
expect(shadow.blurRadius, 4);
});
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);
});
});
}