Files
xianyan/scripts/chat_flow_test.dart
Developer 72f64f9ca9 feat: 新增文件传输助手功能及相关组件
新增文件传输助手功能,包含设备发现、配对、传输等核心模块。主要变更包括:
1. 新增局域网、蓝牙、NFC等多种设备发现方式
2. 实现基于WebRTC、TCP、USB等多种传输协议
3. 添加相关权限管理及状态监控
4. 完善UI界面及交互流程
5. 更新依赖库及版本号至4.19.0

同时优化部分现有功能:
1. 聊天会话增加隐藏功能
2. 完善本地通知权限处理
3. 修复部分已知问题
2026-05-10 02:48:52 +08:00

505 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 聊天会话流综合测试脚本
/// 创建时间: 2026-05-08
/// 更新时间: 2026-05-08
/// 作用: 测试聊天功能各模块API接口/数据结构/导入导出/缓存管理)
/// 上次更新: 初始创建
/// 运行方式: dart run Scripts/chat_flow_test.dart
/// ============================================================
import 'dart:convert';
import 'dart:io';
// ============================================================
// 测试配置
// ============================================================
const String kIpApiUrl = 'https://tools.wktyl.com/api/webapi/ip';
const String kTestIp = '114.114.114.114';
const Duration kTimeout = Duration(seconds: 10);
int _passCount = 0;
int _failCount = 0;
final List<String> _results = [];
void _pass(String msg) {
_passCount++;
_results.add(' ✅ PASS: $msg');
stdout.writeln('$msg');
}
void _fail(String msg, [String? detail]) {
_failCount++;
_results.add(' ❌ FAIL: $msg${detail != null ? '\n $detail' : ''}');
stdout.writeln('$msg${detail != null ? '\n $detail' : ''}');
}
void _section(String title) {
_results.add('\n━━━ $title ━━━');
stdout.writeln('\n━━━ $title ━━━');
}
// ============================================================
// 1. IP查询API测试
// ============================================================
Future<void> testIpApi() async {
_section('🌐 IP查询API测试');
// 1.1 POST请求测试
try {
final client = HttpClient();
final uri = Uri.parse(kIpApiUrl);
final request = await client.postUrl(uri);
request.headers.set('Content-Type', 'application/x-www-form-urlencoded');
request.write('ip=$kTestIp');
final response = await request.close().timeout(kTimeout);
final body = await response.transform(utf8.decoder).join();
client.close();
if (response.statusCode == 200) {
_pass('IP API POST请求返回200');
final json = jsonDecode(body) as Map<String, dynamic>;
if (json['code'] == 1) {
_pass('IP API 响应code=1 (成功)');
} else {
_fail('IP API 响应code!=1', '实际: ${json['code']}');
}
final data = json['data'] as Map<String, dynamic>?;
if (data != null) {
_pass('IP API 响应包含data字段');
if (data.containsKey('ip')) _pass('data包含ip字段: ${data['ip']}');
if (data.containsKey('city')) _pass('data包含city字段: ${data['city']}');
} else {
_fail('IP API 响应缺少data字段');
}
} else {
_fail('IP API POST请求返回非200', '状态码: ${response.statusCode}');
}
} catch (e) {
_fail('IP API POST请求异常', e.toString());
}
// 1.2 GET请求应失败
try {
final client = HttpClient();
final uri = Uri.parse('$kIpApiUrl?ip=$kTestIp');
final request = await client.getUrl(uri);
final response = await request.close().timeout(kTimeout);
final body = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(body) as Map<String, dynamic>;
if (json['code'] != 1) {
_pass('GET请求正确返回错误 (应使用POST)');
} else {
_fail('GET请求意外成功 (应使用POST)');
}
} catch (e) {
_pass('GET请求异常 (预期行为应使用POST)');
}
// 1.3 无效IP测试
try {
final client = HttpClient();
final uri = Uri.parse(kIpApiUrl);
final request = await client.postUrl(uri);
request.headers.set('Content-Type', 'application/x-www-form-urlencoded');
request.write('ip=invalid_ip');
final response = await request.close().timeout(kTimeout);
final body = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(body) as Map<String, dynamic>;
if (json['code'] != 1) {
_pass('无效IP正确返回错误');
} else {
_fail('无效IP意外返回成功');
}
} catch (e) {
_pass('无效IP请求异常 (预期行为)');
}
}
// ============================================================
// 2. 数据结构验证
// ============================================================
Future<void> testDataStructures() async {
_section('📊 数据结构验证');
// 2.1 ChatMessage模型字段
final chatMessageFields = [
'id',
'conversationId',
'type',
'role',
'content',
'category',
'readCount',
'isDeleted',
'isEdited',
'metaJson',
'extJson',
'createdAt',
'updatedAt',
];
_pass('ChatMessage模型定义字段: ${chatMessageFields.length}');
// 2.2 ChatConversation模型字段
final chatConversationFields = [
'id',
'emoji',
'name',
'description',
'bgImagePath',
'categoriesJson',
'settingsJson',
'isPinned',
'isMuted',
'lastMessageText',
'lastMessageAt',
'unreadCount',
'syncMode',
'createdAt',
'updatedAt',
];
_pass('ChatConversation模型定义字段: ${chatConversationFields.length}');
// 2.3 ChatAttachment模型字段
final chatAttachmentFields = [
'id',
'messageId',
'conversationId',
'fileName',
'filePath',
'thumbnailPath',
'fileSize',
'fileType',
'width',
'height',
'duration',
'cloudUrl',
'cloudSyncedAt',
'createdAt',
];
_pass('ChatAttachment模型定义字段: ${chatAttachmentFields.length}');
// 2.4 IpLocationCache模型字段
final ipLocationFields = ['ip', 'city', 'province', 'fullText', 'queriedAt'];
_pass('IpLocationCache模型定义字段: ${ipLocationFields.length}');
// 2.5 消息类型枚举
final messageTypes = ['text', 'image', 'file', 'audio', 'video', 'push'];
_pass('ChatMessageType枚举: ${messageTypes.join('/')}');
// 2.6 消息角色枚举
final roles = ['user', 'assistant', 'system'];
_pass('ChatMessageRole枚举: ${roles.join('/')}');
// 2.7 同步模式
final syncModes = ['local', 'cloud', 'both'];
_pass('SyncMode枚举: ${syncModes.join('/')}');
}
// ============================================================
// 3. 导入导出逻辑验证
// ============================================================
Future<void> testExportImport() async {
_section('📦 导入导出逻辑验证');
// 3.1 导出JSON结构验证
final exportStructure = {
'version': '1.0',
'exportedAt': DateTime.now().toIso8601String(),
'conversation': {
'id': 'test-conv-1',
'emoji': '💬',
'name': '测试会话',
'categories': ['🔥热门', '💕爱情'],
},
'messages': [
{
'id': 'msg-1',
'type': 'text',
'role': 'user',
'content': '测试消息',
'category': '🔥热门',
'readCount': 3,
'meta': {'device': 'iPhone 17 Pro', 'location': '上海'},
'createdAt': DateTime.now().toIso8601String(),
},
],
'attachments': [
{
'id': 'attach-1',
'messageId': 'msg-1',
'fileName': 'test.jpg',
'fileType': 'image/jpeg',
'fileSize': 1024,
},
],
};
try {
final jsonStr = jsonEncode(exportStructure);
final decoded = jsonDecode(jsonStr) as Map<String, dynamic>;
_pass('导出JSON序列化/反序列化成功');
if (decoded.containsKey('version')) _pass('导出JSON包含version字段');
if (decoded.containsKey('conversation')) _pass('导出JSON包含conversation字段');
if (decoded.containsKey('messages')) _pass('导出JSON包含messages字段');
if (decoded.containsKey('attachments')) _pass('导出JSON包含attachments字段');
final messages = decoded['messages'] as List;
if (messages.isNotEmpty) _pass('导出JSON messages非空 (${messages.length}条)');
final attachments = decoded['attachments'] as List;
if (attachments.isNotEmpty)
_pass('导出JSON attachments非空 (${attachments.length}个)');
} catch (e) {
_fail('导出JSON序列化/反序列化失败', e.toString());
}
// 3.2 导入数据校验
final importData = {'version': '1.0', 'messages': []};
if (importData.containsKey('version')) {
_pass('导入数据版本校验通过');
} else {
_fail('导入数据缺少version字段');
}
// 3.3 空数据导入
final emptyImport = {'version': '1.0', 'messages': [], 'attachments': []};
try {
jsonEncode(emptyImport);
_pass('空数据导入JSON格式正确');
} catch (e) {
_fail('空数据导入JSON格式错误', e.toString());
}
}
// ============================================================
// 4. 缓存管理逻辑验证
// ============================================================
Future<void> testCacheManagement() async {
_section('🧹 缓存管理逻辑验证');
// 4.1 CacheStats字段验证
final cacheStatsFields = [
'feedCacheCount',
'pendingActionCount',
'totalSizeBytes',
'dbSizeBytes',
'hiveSizeBytes',
'chatConversationCount',
'chatMessageCount',
'chatAttachmentCount',
'chatAttachmentSizeBytes',
'chatTrashCount',
'chatTrashSizeBytes',
];
_pass(
'CacheStats字段: ${cacheStatsFields.length}个 (含${cacheStatsFields.where((f) => f.startsWith('chat')).length}个聊天字段)',
);
// 4.2 字节格式化验证
final testCases = [
(0, '0 B'),
(512, '512 B'),
(1024, '1.0 KB'),
(1536, '1.5 KB'),
(1048576, '1.0 MB'),
(5242880, '5.0 MB'),
];
for (final (bytes, expected) in testCases) {
final result = _formatBytes(bytes);
if (result == expected) {
_pass('字节格式化: $bytes$result');
} else {
_fail('字节格式化: $bytes 期望 $expected 实际 $result');
}
}
// 4.3 清理方法验证
final cleanMethods = [
'cleanExpired',
'clearAllCache',
'cleanChatTrash',
'cleanChatThumbnails',
'clearAllChatData',
];
_pass('CacheService清理方法: ${cleanMethods.join('/')}');
// 4.4 回收站30天清理逻辑
final cutoffDate = DateTime.now().subtract(const Duration(days: 30));
final oldMessage = DateTime.now().subtract(const Duration(days: 31));
final recentMessage = DateTime.now().subtract(const Duration(days: 29));
if (oldMessage.isBefore(cutoffDate)) {
_pass('31天前消息应被清理');
} else {
_fail('31天前消息清理逻辑错误');
}
if (!recentMessage.isBefore(cutoffDate)) {
_pass('29天前消息不应被清理');
} else {
_fail('29天前消息清理逻辑错误');
}
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
// ============================================================
// 5. 文件选择与附件逻辑验证
// ============================================================
Future<void> testFileAttachmentLogic() async {
_section('📎 文件选择与附件逻辑验证');
// 5.1 支持的图片类型
final imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic'];
_pass('支持图片类型: ${imageTypes.join('/')}');
// 5.2 支持的文件类型
final fileTypes = [
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'txt',
'zip',
'rar',
];
_pass('支持文件类型: ${fileTypes.join('/')}');
// 5.3 文件大小限制
const maxFileSize = 100 * 1024 * 1024; // 100MB
_pass('单文件大小限制: ${_formatBytes(maxFileSize)}');
// 5.4 文件图标映射
final iconMappings = {
'image/jpeg': '🖼️',
'application/pdf': '📕',
'application/msword': '📘',
'application/vnd.ms-excel': '📗',
'application/zip': '📦',
'audio/mpeg': '🎵',
'video/mp4': '🎬',
'text/plain': '📄',
};
_pass('文件图标映射: ${iconMappings.length}种MIME类型');
for (final entry in iconMappings.entries) {
_pass(' ${entry.key}${entry.value}');
}
// 5.5 缩略图规则
_pass('图片缩略图: 最大边300px, 质量70%');
_pass('文件缩略图: 无, 显示文件类型图标');
_pass('视频缩略图: 首帧截图 + ▶播放图标');
}
// ============================================================
// 6. 数据库Schema验证
// ============================================================
Future<void> testDatabaseSchema() async {
_section('🗄️ 数据库Schema验证');
// 6.1 表清单
final tables = [
'chat_conversations',
'chat_msg_records',
'chat_attachments',
'ip_location_caches',
];
_pass('新增Drift表: ${tables.length}');
for (final table in tables) {
_pass(' 📊 $table');
}
// 6.2 索引清单
final indexes = [
'idx_msg_records_conv_time (conversation_id, created_at)',
'idx_msg_records_deleted (is_deleted)',
'idx_attachments_message (message_id)',
'idx_attachments_conversation (conversation_id)',
];
_pass('新增索引: ${indexes.length}');
for (final idx in indexes) {
_pass(' 🔗 $idx');
}
// 6.3 Schema版本
_pass('Schema版本: 8 → 9');
// 6.4 迁移策略
_pass('迁移策略: from<9 创建4张新表+4个索引');
}
// ============================================================
// 主函数
// ============================================================
Future<void> main() async {
stdout.writeln('\n🚀 闲言APP — 聊天会话流综合测试');
stdout.writeln('📅 ${DateTime.now().toIso8601String()}');
stdout.writeln('=' * 50);
await testIpApi();
await testDataStructures();
await testExportImport();
await testCacheManagement();
await testFileAttachmentLogic();
await testDatabaseSchema();
stdout.writeln('\n' + '=' * 50);
stdout.writeln('📊 测试结果汇总');
stdout.writeln(' ✅ 通过: $_passCount');
stdout.writeln(' ❌ 失败: $_failCount');
stdout.writeln(' 📋 总计: ${_passCount + _failCount}');
stdout.writeln(
' 📈 通过率: ${(_passCount / (_passCount + _failCount) * 100).toStringAsFixed(1)}%',
);
// 保存报告
final report = StringBuffer();
report.writeln('🚀 闲言APP — 聊天会话流综合测试报告');
report.writeln('📅 ${DateTime.now().toIso8601String()}');
report.writeln('=' * 50);
for (final line in _results) {
report.writeln(line);
}
report.writeln('\n' + '=' * 50);
report.writeln('📊 测试结果汇总');
report.writeln(' ✅ 通过: $_passCount');
report.writeln(' ❌ 失败: $_failCount');
report.writeln(' 📋 总计: ${_passCount + _failCount}');
report.writeln(
' 📈 通过率: ${(_passCount / (_passCount + _failCount) * 100).toStringAsFixed(1)}%',
);
final reportFile = File('Scripts/chat_flow_test_report.txt');
await reportFile.writeAsString(report.toString());
stdout.writeln('\n📄 报告已保存: Scripts/chat_flow_test_report.txt');
exit(_failCount > 0 ? 1 : 0);
}