358 lines
10 KiB
Dart
358 lines
10 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// 编辑器状态管理测试
|
|
/// 测试撤销/重做、元件管理、画布状态等核心功能
|
|
void main() {
|
|
group('Editor State Management Tests', () {
|
|
// 模拟编辑器状态
|
|
class MockEditorState {
|
|
final List<Map<String, dynamic>> _history = [];
|
|
final List<Map<String, dynamic>> _redoStack = [];
|
|
final List<Map<String, dynamic>> _components = [];
|
|
|
|
int get historyLength => _history.length;
|
|
int get redoStackLength => _redoStack.length;
|
|
int get componentCount => _components.length;
|
|
|
|
bool get canUndo => _history.isNotEmpty;
|
|
bool get canRedo => _redoStack.isNotEmpty;
|
|
|
|
void addComponent(Map<String, dynamic> component) {
|
|
_saveState();
|
|
_components.add(component);
|
|
}
|
|
|
|
void removeComponent(Map<String, dynamic> component) {
|
|
_saveState();
|
|
_components.remove(component);
|
|
}
|
|
|
|
void _saveState() {
|
|
_history.add(List.from(_components));
|
|
_redoStack.clear();
|
|
}
|
|
|
|
bool undo() {
|
|
if (!canUndo) return false;
|
|
_redoStack.add(List.from(_components));
|
|
_components.clear();
|
|
_components.addAll(_history.removeLast() as List<Map<String, dynamic>>);
|
|
return true;
|
|
}
|
|
|
|
bool redo() {
|
|
if (!canRedo) return false;
|
|
_saveState();
|
|
_components.clear();
|
|
_components.addAll(_redoStack.removeLast() as List<Map<String, dynamic>>);
|
|
return true;
|
|
}
|
|
|
|
void clear() {
|
|
_saveState();
|
|
_components.clear();
|
|
}
|
|
}
|
|
|
|
test('initial state should be empty', () {
|
|
final state = MockEditorState();
|
|
|
|
expect(state.historyLength, 0);
|
|
expect(state.redoStackLength, 0);
|
|
expect(state.componentCount, 0);
|
|
expect(state.canUndo, false);
|
|
expect(state.canRedo, false);
|
|
});
|
|
|
|
test('should add component and save state', () {
|
|
final state = MockEditorState();
|
|
|
|
state.addComponent({'ref': 'R1', 'value': '10k'});
|
|
|
|
expect(state.componentCount, 1);
|
|
expect(state.historyLength, 1);
|
|
expect(state.canUndo, true);
|
|
});
|
|
|
|
test('should undo component addition', () {
|
|
final state = MockEditorState();
|
|
|
|
state.addComponent({'ref': 'R1', 'value': '10k'});
|
|
expect(state.componentCount, 1);
|
|
|
|
final result = state.undo();
|
|
|
|
expect(result, true);
|
|
expect(state.componentCount, 0);
|
|
expect(state.canUndo, false);
|
|
expect(state.canRedo, true);
|
|
});
|
|
|
|
test('should redo after undo', () {
|
|
final state = MockEditorState();
|
|
|
|
state.addComponent({'ref': 'R1', 'value': '10k'});
|
|
state.undo();
|
|
expect(state.componentCount, 0);
|
|
|
|
final result = state.redo();
|
|
|
|
expect(result, true);
|
|
expect(state.componentCount, 1);
|
|
expect(state.canRedo, false);
|
|
});
|
|
|
|
test('should clear redo stack on new action', () {
|
|
final state = MockEditorState();
|
|
|
|
state.addComponent({'ref': 'R1', 'value': '10k'});
|
|
state.undo();
|
|
expect(state.canRedo, true);
|
|
|
|
state.addComponent({'ref': 'C1', 'value': '10u'});
|
|
|
|
expect(state.canRedo, false);
|
|
});
|
|
|
|
test('should handle multiple undo/redo', () {
|
|
final state = MockEditorState();
|
|
|
|
state.addComponent({'ref': 'R1', 'value': '10k'});
|
|
state.addComponent({'ref': 'R2', 'value': '20k'});
|
|
state.addComponent({'ref': 'R3', 'value': '30k'});
|
|
|
|
expect(state.componentCount, 3);
|
|
expect(state.historyLength, 3);
|
|
|
|
state.undo();
|
|
expect(state.componentCount, 2);
|
|
|
|
state.undo();
|
|
expect(state.componentCount, 1);
|
|
|
|
state.undo();
|
|
expect(state.componentCount, 0);
|
|
|
|
state.redo();
|
|
expect(state.componentCount, 1);
|
|
|
|
state.redo();
|
|
expect(state.componentCount, 2);
|
|
});
|
|
|
|
test('should handle undo when empty', () {
|
|
final state = MockEditorState();
|
|
|
|
final result = state.undo();
|
|
|
|
expect(result, false);
|
|
expect(state.componentCount, 0);
|
|
});
|
|
|
|
test('should handle redo when empty', () {
|
|
final state = MockEditorState();
|
|
|
|
final result = state.redo();
|
|
|
|
expect(result, false);
|
|
expect(state.componentCount, 0);
|
|
});
|
|
|
|
test('should remove component', () {
|
|
final state = MockEditorState();
|
|
|
|
final component = {'ref': 'R1', 'value': '10k'};
|
|
state.addComponent(component);
|
|
expect(state.componentCount, 1);
|
|
|
|
state.removeComponent(component);
|
|
expect(state.componentCount, 0);
|
|
expect(state.canUndo, true);
|
|
});
|
|
|
|
test('should clear all components', () {
|
|
final state = MockEditorState();
|
|
|
|
state.addComponent({'ref': 'R1', 'value': '10k'});
|
|
state.addComponent({'ref': 'C1', 'value': '10u'});
|
|
expect(state.componentCount, 2);
|
|
|
|
state.clear();
|
|
|
|
expect(state.componentCount, 0);
|
|
expect(state.canUndo, true);
|
|
});
|
|
|
|
test('should limit history size', () {
|
|
final state = MockEditorState();
|
|
const maxHistory = 50;
|
|
|
|
// 添加超过最大历史数的操作
|
|
for (int i = 0; i < 100; i++) {
|
|
state.addComponent({'ref': 'R$i', 'value': '${i}k'});
|
|
}
|
|
|
|
// TODO: 实现历史限制后验证
|
|
// expect(state.historyLength, maxHistory);
|
|
});
|
|
});
|
|
|
|
group('Canvas Transform Tests', () {
|
|
test('should calculate visible rect correctly', () {
|
|
const screenWidth = 400.0;
|
|
const screenHeight = 800.0;
|
|
const zoomLevel = 2.0;
|
|
const offsetX = 100.0;
|
|
const offsetY = 200.0;
|
|
|
|
// 计算可见区域
|
|
final visibleRect = Rect.fromLTWH(
|
|
-offsetX / zoomLevel,
|
|
-offsetY / zoomLevel,
|
|
screenWidth / zoomLevel,
|
|
screenHeight / zoomLevel,
|
|
);
|
|
|
|
expect(visibleRect.left, -50.0);
|
|
expect(visibleRect.top, -100.0);
|
|
expect(visibleRect.width, 200.0);
|
|
expect(visibleRect.height, 400.0);
|
|
});
|
|
|
|
test('should clamp zoom level', () {
|
|
double zoomLevel = 1.0;
|
|
const minZoom = 0.1;
|
|
const maxZoom = 10.0;
|
|
|
|
// 放大
|
|
zoomLevel = (zoomLevel * 1.5).clamp(minZoom, maxZoom);
|
|
expect(zoomLevel, 1.5);
|
|
|
|
// 继续放大到上限
|
|
zoomLevel = (zoomLevel * 10).clamp(minZoom, maxZoom);
|
|
expect(zoomLevel, maxZoom);
|
|
|
|
// 缩小
|
|
zoomLevel = (zoomLevel * 0.1).clamp(minZoom, maxZoom);
|
|
expect(zoomLevel, 1.0);
|
|
|
|
// 继续缩小到下限
|
|
zoomLevel = (zoomLevel * 0.01).clamp(minZoom, maxZoom);
|
|
expect(zoomLevel, minZoom);
|
|
});
|
|
|
|
test('should check if component is visible', () {
|
|
final visibleRect = Rect.fromLTWH(0, 0, 100, 100);
|
|
|
|
// 完全可见
|
|
final component1 = Rect.fromLTWH(10, 10, 20, 20);
|
|
expect(visibleRect.overlaps(component1), true);
|
|
|
|
// 部分可见
|
|
final component2 = Rect.fromLTWH(90, 90, 20, 20);
|
|
expect(visibleRect.overlaps(component2), true);
|
|
|
|
// 完全不可见
|
|
final component3 = Rect.fromLTWH(200, 200, 20, 20);
|
|
expect(visibleRect.overlaps(component3), false);
|
|
});
|
|
});
|
|
|
|
group('Component Validation Tests', () {
|
|
test('should validate ref designator format', () {
|
|
bool isValidRef(String ref) {
|
|
// 位号格式:字母 + 数字,如 R1, C10, U3
|
|
final regex = RegExp(r'^[A-Z]+\d+$');
|
|
return regex.hasMatch(ref);
|
|
}
|
|
|
|
expect(isValidRef('R1'), true);
|
|
expect(isValidRef('C10'), true);
|
|
expect(isValidRef('U3'), true);
|
|
expect(isValidRef('R1A'), false);
|
|
expect(isValidRef('1R'), false);
|
|
expect(isValidRef('R'), false);
|
|
expect(isValidRef(''), false);
|
|
});
|
|
|
|
test('should validate component value format', () {
|
|
bool isValidValue(String value) {
|
|
// 值格式:数字 + 单位(可选),如 10k, 100u, 5V
|
|
final regex = RegExp(r'^\d+(\.\d+)?[a-zA-Z]*$');
|
|
return regex.hasMatch(value);
|
|
}
|
|
|
|
expect(isValidValue('10k'), true);
|
|
expect(isValidValue('100u'), true);
|
|
expect(isValidValue('5V'), true);
|
|
expect(isValidValue('1.5k'), true);
|
|
expect(isValidValue('10'), true);
|
|
expect(isValidValue(''), false);
|
|
expect(isValidValue('abc'), false);
|
|
});
|
|
|
|
test('should validate footprint format', () {
|
|
bool isValidFootprint(String footprint) {
|
|
// 封装格式:数字 + 数字,如 0805, 0603, SOT23
|
|
return footprint.isNotEmpty;
|
|
}
|
|
|
|
expect(isValidFootprint('0805'), true);
|
|
expect(isValidFootprint('0603'), true);
|
|
expect(isValidFootprint('SOT23'), true);
|
|
expect(isValidFootprint(''), false);
|
|
});
|
|
});
|
|
|
|
group('Grid Snapping Tests', () {
|
|
test('should snap to grid', () {
|
|
const gridSize = 10.0;
|
|
|
|
double snapToGrid(double value) {
|
|
return (value / gridSize).round() * gridSize;
|
|
}
|
|
|
|
expect(snapToGrid(5.0), 10.0);
|
|
expect(snapToGrid(12.0), 10.0);
|
|
expect(snapToGrid(17.0), 20.0);
|
|
expect(snapToGrid(25.0), 30.0);
|
|
expect(snapToGrid(0.0), 0.0);
|
|
expect(snapToGrid(-5.0), -10.0);
|
|
});
|
|
|
|
test('should handle different grid sizes', () {
|
|
double snapToGrid(double value, double gridSize) {
|
|
return (value / gridSize).round() * gridSize;
|
|
}
|
|
|
|
expect(snapToGrid(7.0, 5.0), 5.0);
|
|
expect(snapToGrid(7.0, 5.0), 5.0);
|
|
expect(snapToGrid(13.0, 5.0), 15.0);
|
|
});
|
|
});
|
|
|
|
group('Wire Connection Tests', () {
|
|
test('should check if points are connected', () {
|
|
const snapDistance = 5.0;
|
|
|
|
bool areConnected(Offset p1, Offset p2) {
|
|
return (p1 - p2).distance <= snapDistance;
|
|
}
|
|
|
|
expect(areConnected(const Offset(0, 0), const Offset(3, 4)), true); // distance = 5
|
|
expect(areConnected(const Offset(0, 0), const Offset(6, 8)), false); // distance = 10
|
|
expect(areConnected(const Offset(0, 0), const Offset(0, 0)), true);
|
|
});
|
|
|
|
test('should calculate wire length', () {
|
|
double wireLength(Offset p1, Offset p2) {
|
|
return (p2 - p1).distance;
|
|
}
|
|
|
|
expect(wireLength(const Offset(0, 0), const Offset(3, 4)), 5.0);
|
|
expect(wireLength(const Offset(0, 0), const Offset(10, 0)), 10.0);
|
|
});
|
|
});
|
|
}
|