Files
jizhi/frontend/test/visual_contract_test.dart

347 lines
15 KiB
Dart
Raw Permalink 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.
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('登录强制刷新远端资料并按后端隔离本地账号数据', () {
final auth = File('lib/shared/api/auth_api.dart').readAsStringSync();
final session = File(
'lib/shared/services/session_store.dart',
).readAsStringSync();
expect(auth, contains('me(forceRemote: true)'));
expect(auth, contains('if (!forceRemote &&'));
expect(session, contains('BackendIdentity.accountNamespace(userId)'));
expect(session, contains('pending_account_archive_namespace'));
expect(session, contains('last_account_namespace'));
final client = File('lib/shared/api/api_client.dart').readAsStringSync();
expect(client, contains("'auth_token_\${BackendIdentity.scope}'"));
});
test('启动页只读取本地缓存,不等待远程接口', () {
final source = File(
'lib/features/auth/pages/splash_page.dart',
).readAsStringSync();
expect(source, contains('loadCached'));
expect(source, isNot(contains('PublicConfigApi')));
expect(source, isNot(contains('AuthApi.me')));
expect(source, isNot(contains('ensureLoaded')));
});
test('业务页面不再直接使用系统感强的控件或旧青绿色板', () {
final banned = <String>[
'AlertDialog(',
'DropdownButtonFormField',
'showDatePicker(',
'showTimePicker(',
'PopupMenuButton',
'LinearGradient(',
'RadialGradient(',
'SweepGradient(',
'BackdropFilter(',
'0xFF087F6D',
'0xFF066B5C',
'0xFFE3F2EF',
];
final violations = <String>[];
for (final file
in Directory('lib/features')
.listSync(recursive: true)
.whereType<File>()
.where((file) => file.path.endsWith('.dart'))) {
final source = file.readAsStringSync();
for (final token in banned) {
if (source.contains(token)) {
violations.add('${file.path}: $token');
}
}
}
expect(violations, isEmpty, reason: violations.join('\n'));
});
test('深色模式的选中与未选中状态使用语义色板', () {
final controls = File(
'lib/shared/widgets/app_controls.dart',
).readAsStringSync();
final addPage = File('lib/features/add/add_page.dart').readAsStringSync();
final chat = File('lib/features/chat/chat_page.dart').readAsStringSync();
final companion = File(
'lib/features/settings/companion_page.dart',
).readAsStringSync();
final onboarding = File(
'lib/features/onboarding/pages/onboarding_page.dart',
).readAsStringSync();
final me = File('lib/features/settings/me_page.dart').readAsStringSync();
final report = File(
'lib/features/stats/report_page.dart',
).readAsStringSync();
expect(controls, contains(': context.jz.card'));
expect(addPage, isNot(contains('selected ? _activeColor : Colors.white')));
expect(chat, isNot(contains('isMe ? AppTheme.primary : Colors.white')));
expect(
companion,
isNot(contains('context.jz.aiBackground : Colors.white')),
);
expect(onboarding, contains(': context.jz.card'));
expect(
onboarding,
isNot(contains('context.jz.aiBackground : Colors.white')),
);
expect(me, isNot(contains('context.jz.primaryBackground : Colors.white')));
expect(report, isNot(contains('selected ? Colors.white')));
});
test('首次引导只提交服务端实际返回的 AI 伙伴配置', () {
final onboarding = File(
'lib/features/onboarding/pages/onboarding_page.dart',
).readAsStringSync();
expect(onboarding, contains('_hasValidCatalogSelection'));
expect(onboarding, contains('AI 伙伴配置暂不可用'));
expect(onboarding, contains('重新加载'));
expect(onboarding, isNot(contains('_fallbackAvatars')));
expect(onboarding, isNot(contains('_fallbackPersonas')));
});
test('聊天附件只保留拍照和相册导入', () {
final source = File('lib/features/chat/chat_page.dart').readAsStringSync();
expect(source, contains('ImageSource.camera'));
expect(source, contains('ImageSource.gallery'));
expect(source, isNot(contains('截屏记账')));
expect(source, isNot(contains('手动输入文字')));
expect(source, isNot(contains('ScreenshotChannel.capture')));
});
test('底部导航固定保留 AI 入口且关闭状态在聊天二级页处理', () {
final shell = File(
'lib/features/home/pages/main_shell.dart',
).readAsStringSync();
final router = File('lib/app/app.dart').readAsStringSync();
final gate = File(
'lib/shared/widgets/ai_access_gate.dart',
).readAsStringSync();
expect(shell, isNot(contains('showAiEntry')));
expect(shell, contains('_tab(companion.name, AppIcons.chat, 2)'));
expect(router, isNot(contains("state.matchedLocation == '/chat'")));
expect(gate, contains('AiAccessState.aiDisabled'));
expect(gate, contains('AI 功能已关闭'));
expect(gate, contains('手动记账和统计不受影响'));
});
test('报告入口归入统计并继承当前周期', () {
final stats = File('lib/features/stats/stats_page.dart').readAsStringSync();
final report = File(
'lib/features/stats/report_page.dart',
).readAsStringSync();
final me = File('lib/features/settings/me_page.dart').readAsStringSync();
expect(me, isNot(contains("context.push('/report')")));
expect(stats, contains("path: '/report'"));
expect(stats, contains("'period': _period"));
expect(stats, contains("'date': _anchor.toIso8601String()"));
expect(stats, contains('查看完整报告'));
expect(report, contains('widget.initialPeriod'));
expect(report, contains('widget.initialAnchor'));
});
test('聊天输入为空显示附件加号,有文本时显示发送按钮', () {
final chat = File('lib/features/chat/chat_page.dart').readAsStringSync();
expect(chat, contains("ValueKey('add-attachment')"));
expect(chat, contains("ValueKey('send-message')"));
expect(chat, contains('child: hasText'));
expect(chat, contains("tooltip: '添加附件'"));
expect(chat, isNot(contains("'附件'")));
});
test('表情包具备离线缓存、内置兜底和展开刷新', () {
final api = File('lib/shared/api/business_api.dart').readAsStringSync();
final chat = File('lib/features/chat/chat_page.dart').readAsStringSync();
expect(api, contains('global_stickers_v1'));
expect(api, contains('_fallback'));
expect(api, contains('SharedPreferences.getInstance'));
expect(
chat,
contains('setState(() => _composerPanel = _ComposerPanel.stickers)'),
);
expect(chat, contains('_stkLoad();'));
});
test('无障碍只解析明确成功页且使用墙钟时间', () {
final parser = File(
'android/app/src/main/kotlin/com/nx/miaoji/PaymentParser.kt',
).readAsStringSync();
expect(parser, contains('occurredAt = System.currentTimeMillis()'));
expect(parser, contains('historyTitles'));
expect(parser, contains('"扫码支付成功"'));
expect(parser, contains('"转账已完成"'));
expect(parser, contains('STANDALONE_AMOUNT'));
expect(parser, contains('MAX_PAYMENT_SCAN_LINES = 48'));
expect(parser, isNot(contains('listOf("收款成功", "已收款", "到账"')));
expect(parser, isNot(contains('"支出", "消费成功"')));
expect(parser, isNot(contains('listOf("微信支付", "支付宝", "交易成功")')));
final service = File(
'android/app/src/main/kotlin/com/nx/miaoji/ScreenshotAccessibilityService.kt',
).readAsStringSync();
expect(service, contains('AccessibilityEvent.TYPE_WINDOWS_CHANGED'));
expect(service, contains('AccessibilityEvent.TYPE_VIEW_CLICKED'));
expect(service, contains('PaymentFlow('));
expect(service, contains('PAYMENT_FLOW_TTL_MS = 90_000L'));
expect(service, contains('VISUAL_STABILITY_DELAY_MS = 700L'));
expect(service, contains('takeScreenshotOfWindow'));
expect(service, contains('bitmap.recycle()'));
expect(service, contains('CAPTURE_CALLBACK_TIMEOUT_MS = 6_000L'));
expect(service, contains('OCR_CALLBACK_TIMEOUT_MS = 12_000L'));
final localOcr = File(
'android/app/src/main/kotlin/com/nx/miaoji/LocalPaymentOcr.kt',
).readAsStringSync();
expect(localOcr, contains('ChineseTextRecognizerOptions'));
expect(localOcr, contains('InputImage.fromBitmap'));
expect(localOcr, contains('distinctCents.size == 1'));
expect(localOcr, contains('evidenceConfidence = if (highConfidence)'));
final store = File(
'android/app/src/main/kotlin/com/nx/miaoji/RecognitionStore.kt',
).readAsStringSync();
expect(store, contains('"local_ocr" -> 8'));
expect(store, contains('flowSessionId'));
expect(store, contains('ACCESSIBILITY_NOTIFICATION_MASK'));
final gradle = File('android/app/build.gradle.kts').readAsStringSync();
expect(
gradle,
contains('com.google.mlkit:text-recognition-chinese:16.0.1'),
);
});
test('本地 OCR 需要一次性用途确认并提供无隐私诊断', () {
final settings = File(
'lib/features/settings/screenshot_settings_page.dart',
).readAsStringSync();
final legal = File(
'lib/features/settings/legal_document_page.dart',
).readAsStringSync();
final diagnostics = File(
'android/app/src/main/kotlin/com/nx/miaoji/RecognitionDiagnostics.kt',
).readAsStringSync();
final formatter = File(
'lib/shared/services/recognition_diagnostic_formatter.dart',
).readAsStringSync();
expect(settings, contains('local_ocr_consent_v1'));
expect(settings, contains('图片只在内存中由本地 OCR 处理'));
expect(settings, contains('_RecognitionDiagnosticCard'));
expect(settings, contains('Timer.periodic'));
expect(formatter, contains('operation_interrupted'));
expect(formatter, contains('截图失败'));
expect(formatter, contains('OCR 无结果'));
expect(formatter, contains('规则拒绝'));
expect(formatter, contains('已合并'));
expect(legal, contains('ML Kit 中文文字识别(Google'));
expect(diagnostics, isNot(contains('sourceText')));
expect(diagnostics, isNot(contains('image')));
expect(diagnostics, contains('STALE_OPERATION_MS = 20_000L'));
final service = File(
'android/app/src/main/kotlin/com/nx/miaoji/ScreenshotAccessibilityService.kt',
).readAsStringSync();
expect(service, contains('resolveAccessibilityWindowId'));
final localOcr = File(
'android/app/src/main/kotlin/com/nx/miaoji/LocalPaymentOcr.kt',
).readAsStringSync();
expect(localOcr, contains('MlKitContext.initializeIfNeeded'));
expect(localOcr, contains('ocr_model_unavailable'));
expect(localOcr, contains('OcrDiagnosticRedactor'));
expect(localOcr, contains('qualifiesWeakAuto'));
expect(settings, contains('OCR 诊断预览'));
expect(settings, contains('10 分钟后自动关闭'));
expect(diagnostics, contains('statusStrength'));
expect(diagnostics, contains('expectedAmountMatched'));
expect(diagnostics, contains('resultTransitionObserved'));
expect(diagnostics, contains('ocrPreview'));
expect(service, contains('shouldFallbackToDisplay'));
expect(service, contains('ERROR_TAKE_SCREENSHOT_INVALID_WINDOW'));
expect(service, contains('WINDOW_CAPTURE_FALLBACK_DELAY_MS = 450L'));
});
test('智能识别授权未完成时不会提前打开开关', () {
final source = File(
'lib/features/settings/screenshot_settings_page.dart',
).readAsStringSync();
expect(source, contains('_pendingAuthorizationKey'));
expect(source, contains('未完成系统授权,识别开关仍保持关闭'));
expect(source, contains('setRecognitionToggle(key, false)'));
expect(source, contains('_BackgroundKeepAliveCard'));
});
test('长期识别组件使用独立保活进程并支持非阻塞重连', () {
final manifest = File(
'android/app/src/main/AndroidManifest.xml',
).readAsStringSync();
final channel = File(
'lib/shared/services/screenshot_channel.dart',
).readAsStringSync();
final activity = File(
'android/app/src/main/kotlin/com/nx/miaoji/MainActivity.kt',
).readAsStringSync();
final settingsPage = File(
'lib/features/settings/screenshot_settings_page.dart',
).readAsStringSync();
final keepAlive = File(
'android/app/src/main/kotlin/com/nx/miaoji/RecognitionKeepAliveService.kt',
).readAsStringSync();
for (final component in [
'ScreenshotTileService',
'PaymentNotificationListenerService',
'ScreenshotAccessibilityService',
]) {
final declaration = RegExp(
'<service\\s+android:name="\\.$component"[\\s\\S]*?</service>',
).firstMatch(manifest)?.group(0);
expect(declaration, isNotNull);
expect(declaration, contains('android:stopWithTask="false"'));
expect(declaration, contains('android:process=":recognition"'));
}
final provider = RegExp(
'<provider\\s+android:name="\\.RecognitionBridgeProvider"[\\s\\S]*?/>',
).firstMatch(manifest)?.group(0);
expect(provider, isNotNull);
expect(provider, contains('android:process=":recognition"'));
expect(manifest, contains('android:name=".OneShotProjectionService"'));
final projection = RegExp(
'<service\\s+android:name="\\.OneShotProjectionService"[\\s\\S]*?/>',
).firstMatch(manifest)?.group(0);
expect(projection, isNotNull);
expect(projection, contains('android:process=":projection"'));
expect(manifest, contains('android:name=".RecognitionKeepAliveService"'));
expect(manifest, contains('android:foregroundServiceType="specialUse"'));
expect(manifest, contains('FOREGROUND_SERVICE_SPECIAL_USE'));
expect(manifest, contains('PROPERTY_SPECIAL_USE_FGS_SUBTYPE'));
expect(keepAlive, contains('START_STICKY'));
expect(keepAlive, contains('智能识别运行中'));
expect(channel, contains('waitForAccessibilityConnection'));
expect(channel, contains('_activeAccessibilityConnectionWait'));
expect(channel, contains('ensureRecognitionKeepAlive'));
expect(channel, contains('AccessibilityConnectionState.reconnecting'));
expect(activity, contains('"reconnecting"'));
expect(activity, contains('"disconnected"'));
expect(activity, contains('setExcludeFromRecents'));
expect(activity, contains('getHistoricalProcessExitReasons'));
expect(activity, contains('single-cleaner'));
expect(settingsPage, contains('_check();'));
expect(settingsPage, isNot(contains('_check(waitForConnection: true)')));
expect(settingsPage, contains('系统仍显示已授权,但 OriginOS 未重新绑定服务'));
expect(settingsPage, contains('启动后台保护'));
expect(settingsPage, contains('OriginOS 清理了识别进程'));
expect(settingsPage, contains('记之已从最近任务隐藏'));
expect(channel, contains('taskCleanerRecoveryNeeded'));
});
}