feat: 新增多模块后端管理、数据同步工具与鸿蒙路由适配

本次提交新增了以下核心内容:
1. 后端管理模块:包含字体同步、插件元数据、插件用户设置、稍后读消息/共享列表的控制器、模型、验证器与多语言配置
2. Flutter数据同步模块:统一的事件总线与兼容层,替代分散的StreamController
3. 鸿蒙端路由适配:完整的路由定义、构建器与占位组件
4. 后端API接口:字体同步与插件更新的服务端API,支持自动建表与跨域请求
5. 鸿蒙权限校验脚本:用于校验module.json5与string.json的权限声明一致性
This commit is contained in:
Developer
2026-06-01 05:50:13 +08:00
parent 9ea8d3d606
commit 5a083bdbab
159 changed files with 15621 additions and 10565 deletions

View File

@@ -0,0 +1,682 @@
// ============================================================
// 闲言APP — 协作画布CanvasProvider单元测试
// 创建时间: 2026-06-01
// 更新时间: 2026-06-01
// 作用: 测试CanvasState初始状态、工具/颜色/线宽设置、
// 笔画创建/撤销/重做/清空、画布加入/离开、dispose防护
// 上次更新: 重写Mock为@override方式解决mocktail when()冲突
// ============================================================
import 'dart:async';
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xianyan/features/file_transfer/collaboration/canvas/models/stroke.dart';
import 'package:xianyan/features/file_transfer/collaboration/canvas/providers/canvas_provider.dart';
import 'package:xianyan/features/file_transfer/providers/shared_signaling_provider.dart';
import 'package:xianyan/features/file_transfer/services/signaling_service.dart';
// ============================================================
// 测试用SignalingService@override方式避免mocktail when()冲突
// CanvasSyncService仅使用isConnected/onMessage/sendCustomMessage
// ============================================================
class TestSignalingService implements SignalingService {
bool _isConnected = false;
final StreamController<SignalingMessage> _messageController =
StreamController<SignalingMessage>.broadcast();
final List<SignalingMessage> sentMessages = [];
void setConnected(bool value) => _isConnected = value;
@override
bool get isConnected => _isConnected;
@override
Stream<SignalingMessage> get onMessage => _messageController.stream;
@override
void sendCustomMessage(SignalingMessage message) {
sentMessages.add(message);
}
@override
dynamic noSuchMethod(Invocation invocation) => null;
void close() {
_messageController.close();
}
}
// ============================================================
// 测试主体
// ============================================================
void main() {
late TestSignalingService testSignaling;
late ProviderContainer container;
// ----------------------------------------------------------
// 初始化:每个测试前创建测试信令服务和容器
// ----------------------------------------------------------
setUp(() {
testSignaling = TestSignalingService();
container = ProviderContainer(
overrides: [
sharedSignalingProvider.overrideWithValue(testSignaling),
],
);
});
// ----------------------------------------------------------
// 清理:每个测试后销毁容器和信令服务
// ----------------------------------------------------------
tearDown(() {
container.dispose();
testSignaling.close();
});
// ----------------------------------------------------------
// 辅助方法等待microtask队列清空
// CanvasNotifier通过Future.microtask异步更新state
// 测试中需要等待microtask完成后才能断言state
// ----------------------------------------------------------
Future<void> flushMicrotasks({int rounds = 3}) async {
for (var i = 0; i < rounds; i++) {
await Future.microtask(() {});
}
}
// ===========================================================
// 1. CanvasState 初始状态
// ===========================================================
group('CanvasState 初始状态', () {
test('所有字段应为默认值', () {
final state = container.read(canvasProvider);
expect(state.strokes, isEmpty);
expect(state.activeStroke, isNull);
expect(state.currentTool, StrokeType.pen);
expect(state.currentColor, '#000000');
expect(state.currentWidth, 3.0);
expect(state.remoteCursors, isEmpty);
expect(state.cursorOpacities, isEmpty);
expect(state.participants, isEmpty);
expect(state.isConnected, isFalse);
expect(state.isSyncing, isFalse);
expect(state.canvasId, isNull);
});
});
// ===========================================================
// 2. setTool
// ===========================================================
group('setTool', () {
test('正确更新currentTool为eraser', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setTool(StrokeType.eraser);
await flushMicrotasks();
expect(notifier.state.currentTool, StrokeType.eraser);
});
test('正确更新currentTool为circle', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setTool(StrokeType.circle);
await flushMicrotasks();
expect(notifier.state.currentTool, StrokeType.circle);
});
test('遍历所有StrokeType均能正确设置', () async {
final notifier = container.read(canvasProvider.notifier);
for (final tool in StrokeType.values) {
notifier.setTool(tool);
await flushMicrotasks();
expect(notifier.state.currentTool, tool);
}
});
});
// ===========================================================
// 3. setColor
// ===========================================================
group('setColor', () {
test('正确更新currentColor', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setColor('#FF0000');
await flushMicrotasks();
expect(notifier.state.currentColor, '#FF0000');
});
test('多次设置颜色保留最后一次', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setColor('#FF0000');
await flushMicrotasks();
notifier.setColor('#00FF00');
await flushMicrotasks();
expect(notifier.state.currentColor, '#00FF00');
});
});
// ===========================================================
// 4. setWidth
// ===========================================================
group('setWidth', () {
test('正确更新currentWidth', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setWidth(8.0);
await flushMicrotasks();
expect(notifier.state.currentWidth, 8.0);
});
test('设置极小线宽', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setWidth(0.5);
await flushMicrotasks();
expect(notifier.state.currentWidth, 0.5);
});
});
// ===========================================================
// 5. startStroke
// ===========================================================
group('startStroke', () {
test('创建新的activeStroke', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(10, 20));
await flushMicrotasks();
final active = notifier.state.activeStroke;
expect(active, isNotNull);
expect(active!.points, isNotEmpty);
expect(active.points.first, const Offset(10, 20));
expect(active.type, StrokeType.pen);
expect(active.color, '#000000');
});
test('eraser工具创建白色粗笔画', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setTool(StrokeType.eraser);
await flushMicrotasks();
notifier.startStroke(const Offset(5, 5));
await flushMicrotasks();
final active = notifier.state.activeStroke;
expect(active, isNotNull);
expect(active!.color, '#FFFFFF');
expect(active.width, greaterThan(3.0));
});
test('使用当前颜色和线宽', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.setColor('#0000FF');
notifier.setWidth(6.0);
await flushMicrotasks();
notifier.startStroke(const Offset(0, 0));
await flushMicrotasks();
final active = notifier.state.activeStroke;
expect(active, isNotNull);
expect(active!.color, '#0000FF');
expect(active.width, 6.0);
});
});
// ===========================================================
// 6. addPoint
// ===========================================================
group('addPoint', () {
test('向activeStroke添加点', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
await flushMicrotasks();
notifier.addPoint(const Offset(10, 10));
await flushMicrotasks();
final active = notifier.state.activeStroke;
expect(active, isNotNull);
expect(active!.points.length, 2);
expect(active.points.last, const Offset(10, 10));
});
test('连续添加多个点', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
await flushMicrotasks();
for (var i = 1; i <= 5; i++) {
notifier.addPoint(Offset(i * 10.0, i * 10.0));
}
await flushMicrotasks();
expect(notifier.state.activeStroke!.points.length, 6);
});
});
// ===========================================================
// 7. endStroke
// ===========================================================
group('endStroke', () {
test('将activeStroke移到strokes列表', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
await flushMicrotasks();
notifier.endStroke();
await flushMicrotasks();
expect(notifier.state.activeStroke, isNull);
expect(notifier.state.strokes.length, 1);
expect(notifier.state.strokes.first.points.length, 2);
});
test('少于2个点时endStroke不添加到strokes', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
await flushMicrotasks();
notifier.endStroke();
await flushMicrotasks();
expect(notifier.state.activeStroke, isNull);
expect(notifier.state.strokes, isEmpty);
});
test('无activeStroke时endStroke不报错', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.endStroke();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
test('多笔绘制后strokes列表递增', () async {
final notifier = container.read(canvasProvider.notifier);
for (var i = 0; i < 3; i++) {
notifier.startStroke(Offset(i * 10.0, 0));
notifier.addPoint(Offset(i * 10.0, 10.0));
await flushMicrotasks();
notifier.endStroke();
await flushMicrotasks();
}
expect(notifier.state.strokes.length, 3);
});
});
// ===========================================================
// 8. undo
// ===========================================================
group('undo', () {
test('撤销最后一笔', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
expect(notifier.state.strokes.length, 1);
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
test('多笔时只撤销最后一笔', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
notifier.startStroke(const Offset(20, 20));
notifier.addPoint(const Offset(30, 30));
notifier.endStroke();
await flushMicrotasks();
expect(notifier.state.strokes.length, 2);
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes.length, 1);
});
test('无可撤销笔画时undo不报错', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
});
// ===========================================================
// 9. redo
// ===========================================================
group('redo', () {
test('恢复撤销的笔画', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
notifier.redo();
await flushMicrotasks();
expect(notifier.state.strokes.length, 1);
});
test('新笔画后redo栈被清空无法redo', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
notifier.undo();
await flushMicrotasks();
notifier.startStroke(const Offset(20, 20));
notifier.addPoint(const Offset(30, 30));
notifier.endStroke();
await flushMicrotasks();
notifier.redo();
await flushMicrotasks();
expect(notifier.state.strokes.length, 1);
});
test('无可用redo时redo不报错', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.redo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
test('undo→redo→undo循环正确', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
notifier.redo();
await flushMicrotasks();
expect(notifier.state.strokes.length, 1);
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
});
// ===========================================================
// 10. clearCanvas
// ===========================================================
group('clearCanvas', () {
test('清空所有笔画', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
notifier.startStroke(const Offset(20, 20));
notifier.addPoint(const Offset(30, 30));
notifier.endStroke();
await flushMicrotasks();
expect(notifier.state.strokes.length, 2);
notifier.clearCanvas();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
test('清空后undo和redo均不可用', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
notifier.addPoint(const Offset(10, 10));
notifier.endStroke();
await flushMicrotasks();
notifier.clearCanvas();
await flushMicrotasks();
notifier.undo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
notifier.redo();
await flushMicrotasks();
expect(notifier.state.strokes, isEmpty);
});
test('清空后activeStroke为null', () async {
final notifier = container.read(canvasProvider.notifier);
notifier.startStroke(const Offset(0, 0));
await flushMicrotasks();
expect(notifier.state.activeStroke, isNotNull);
notifier.clearCanvas();
await flushMicrotasks();
expect(notifier.state.activeStroke, isNull);
});
});
// ===========================================================
// 11. _disposed 防护
// ===========================================================
group('dispose防护', () {
test('dispose后_disposed阻止microtask状态更新不抛异常', () async {
final testSignaling2 = TestSignalingService();
final testContainer = ProviderContainer(
overrides: [
sharedSignalingProvider.overrideWithValue(testSignaling2),
],
);
final notifier = testContainer.read(canvasProvider.notifier);
notifier.setTool(StrokeType.eraser);
testContainer.dispose();
testSignaling2.close();
await flushMicrotasks();
});
test('leaveCanvas在dispose后不更新状态', () async {
final testSignaling2 = TestSignalingService();
testSignaling2.setConnected(true);
final testContainer = ProviderContainer(
overrides: [
sharedSignalingProvider.overrideWithValue(testSignaling2),
],
);
final notifier = testContainer.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas', 'device-1');
notifier.leaveCanvas();
testContainer.dispose();
testSignaling2.close();
await flushMicrotasks();
});
});
// ===========================================================
// 12. joinCanvas
// ===========================================================
group('joinCanvas', () {
test('设置canvasId和isConnected', () {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas-123', 'device-A');
expect(notifier.state.canvasId, 'test-canvas-123');
expect(notifier.state.isConnected, isTrue);
});
test('joinCanvas后participants包含当前设备', () async {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas', 'device-A');
await flushMicrotasks();
expect(notifier.state.participants, contains('device-A'));
});
test('重复joinCanvas覆盖旧canvasId', () {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('canvas-1', 'device-A');
notifier.joinCanvas('canvas-2', 'device-A');
expect(notifier.state.canvasId, 'canvas-2');
});
});
// ===========================================================
// 13. leaveCanvas
// ===========================================================
group('leaveCanvas', () {
test('清除isConnected', () async {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas', 'device-A');
expect(notifier.state.isConnected, isTrue);
notifier.leaveCanvas();
await flushMicrotasks();
expect(notifier.state.isConnected, isFalse);
});
test('清除canvasId', () async {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas', 'device-A');
expect(notifier.state.canvasId, 'test-canvas');
notifier.leaveCanvas();
await flushMicrotasks();
expect(notifier.state.canvasId, isNull);
});
test('清除remoteCursors和cursorOpacities', () async {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas', 'device-A');
await flushMicrotasks();
notifier.leaveCanvas();
await flushMicrotasks();
expect(notifier.state.remoteCursors, isEmpty);
expect(notifier.state.cursorOpacities, isEmpty);
});
test('清除participants', () async {
testSignaling.setConnected(true);
final notifier = container.read(canvasProvider.notifier);
notifier.joinCanvas('test-canvas', 'device-A');
await flushMicrotasks();
notifier.leaveCanvas();
await flushMicrotasks();
expect(notifier.state.participants, isEmpty);
});
});
// ===========================================================
// CanvasState.copyWith 额外测试
// ===========================================================
group('CanvasState.copyWith', () {
test('clearCanvasId将canvasId置为null', () {
const state = CanvasState(canvasId: 'test-id');
final updated = state.copyWith(clearCanvasId: true);
expect(updated.canvasId, isNull);
});
test('clearCanvasId为false时保留原值', () {
const state = CanvasState(canvasId: 'test-id');
final updated = state.copyWith(clearCanvasId: false);
expect(updated.canvasId, 'test-id');
});
test('clearActiveStroke将activeStroke置为null', () {
final stroke = Stroke(
id: 's1',
userId: 'u1',
color: '#000000',
width: 3.0,
points: [const Offset(0, 0)],
lamportClock: 1,
type: StrokeType.pen,
createdAt: DateTime.now(),
);
final state = CanvasState(activeStroke: stroke);
final updated = state.copyWith(clearActiveStroke: true);
expect(updated.activeStroke, isNull);
});
test('部分字段更新不影响其他字段', () {
const state = CanvasState(
currentColor: '#FF0000',
currentWidth: 5.0,
isConnected: true,
);
final updated = state.copyWith(currentColor: '#00FF00');
expect(updated.currentColor, '#00FF00');
expect(updated.currentWidth, 5.0);
expect(updated.isConnected, isTrue);
});
});
// ===========================================================
// StrokeType枚举测试
// ===========================================================
group('StrokeType', () {
test('fromId正确映射所有枚举值', () {
expect(StrokeType.fromId('pen'), StrokeType.pen);
expect(StrokeType.fromId('eraser'), StrokeType.eraser);
expect(StrokeType.fromId('line'), StrokeType.line);
expect(StrokeType.fromId('rect'), StrokeType.rect);
expect(StrokeType.fromId('circle'), StrokeType.circle);
expect(StrokeType.fromId('text'), StrokeType.text);
});
test('fromId未知值返回pen', () {
expect(StrokeType.fromId('unknown'), StrokeType.pen);
expect(StrokeType.fromId(''), StrokeType.pen);
});
});
}