150 lines
4.6 KiB
Dart
150 lines
4.6 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:miaoji_zhang/shared/api/api_client.dart';
|
|
import 'package:miaoji_zhang/shared/api/auth_api.dart';
|
|
import 'package:miaoji_zhang/shared/widgets/backend_status_icon.dart';
|
|
|
|
void main() {
|
|
tearDown(() {
|
|
ApiClient.availability.value = BackendAvailability.unknown;
|
|
});
|
|
|
|
testWidgets('离线状态只显示可点击且可访问的云朵重试图标', (tester) async {
|
|
var retries = 0;
|
|
ApiClient.availability.value = BackendAvailability.offline;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
actions: [
|
|
BackendStatusIcon(
|
|
onRetry: () async {
|
|
retries++;
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final retry = find.byTooltip('当前显示本地数据,点击重试');
|
|
expect(retry, findsOneWidget);
|
|
await tester.tap(retry);
|
|
await tester.pump();
|
|
expect(retries, 1);
|
|
|
|
ApiClient.availability.value = BackendAvailability.online;
|
|
await tester.pump();
|
|
expect(retry, findsNothing);
|
|
});
|
|
|
|
test('连接、发送和接收失败都会被识别为后端不可达', () {
|
|
DioException failure(DioExceptionType type) => DioException(
|
|
requestOptions: RequestOptions(path: '/test'),
|
|
type: type,
|
|
);
|
|
|
|
expect(
|
|
isConnectivityError(failure(DioExceptionType.connectionError)),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
isConnectivityError(failure(DioExceptionType.connectionTimeout)),
|
|
isTrue,
|
|
);
|
|
expect(isConnectivityError(failure(DioExceptionType.sendTimeout)), isTrue);
|
|
expect(
|
|
isConnectivityError(failure(DioExceptionType.receiveTimeout)),
|
|
isTrue,
|
|
);
|
|
expect(isConnectivityError(failure(DioExceptionType.badResponse)), isFalse);
|
|
});
|
|
|
|
test('AI 伙伴缓存 DTO 可无损往返', () {
|
|
final source = {
|
|
'avatarKey': 'cat',
|
|
'personaKey': 'gentle',
|
|
'customName': '小记',
|
|
'roastLevel': 20,
|
|
'stickerFrequency': 40,
|
|
'proactiveLevel': 60,
|
|
};
|
|
final companion = AiCompanion.fromJson(source);
|
|
|
|
expect(companion.toJson(), source);
|
|
});
|
|
|
|
test('七个页面保持本地首屏、后台刷新、竞态保护和统一离线入口', () {
|
|
final contracts = <String, List<String>>{
|
|
'lib/features/home/pages/home_page.dart': [
|
|
'monthLocal',
|
|
'monthRemote',
|
|
'BudgetApi.getLocal',
|
|
'BudgetApi.getRemote',
|
|
'refreshRemote',
|
|
],
|
|
'lib/features/add/add_page.dart': [
|
|
"categoriesLocal('expense')",
|
|
"categoriesRemote('expense')",
|
|
"categoriesRemote('income')",
|
|
],
|
|
'lib/features/stats/stats_page.dart': [
|
|
'periodStatsLocal',
|
|
'periodStatsRemote',
|
|
],
|
|
'lib/features/stats/report_page.dart': [
|
|
'ReportApi.local',
|
|
'ReportApi.remote',
|
|
],
|
|
'lib/features/settings/category_manage_page.dart': [
|
|
'categoriesLocal',
|
|
'categoriesRemote',
|
|
],
|
|
'lib/features/settings/recycle_bin_page.dart': [
|
|
'recycleBinLocal',
|
|
'recycleBinRemote',
|
|
],
|
|
'lib/features/settings/companion_page.dart': [
|
|
'cachedCompanion',
|
|
'cachedAvatars',
|
|
'cachedPersonas',
|
|
'forceRemote: true',
|
|
],
|
|
};
|
|
|
|
for (final entry in contracts.entries) {
|
|
final source = File(entry.key).readAsStringSync();
|
|
expect(source, contains('_loadRevision'), reason: entry.key);
|
|
expect(source, contains('BackendStatusIcon'), reason: entry.key);
|
|
for (final token in entry.value) {
|
|
expect(source, contains(token), reason: '${entry.key}: $token');
|
|
}
|
|
}
|
|
|
|
final companion = File(
|
|
'lib/features/settings/companion_page.dart',
|
|
).readAsStringSync();
|
|
expect(companion, isNot(contains('if (!_loaded)')));
|
|
expect(companion, contains('BackendAvailability.online'));
|
|
});
|
|
|
|
test('AI 缓存按后端和账号隔离,回收站破坏性操作只允许在线执行', () {
|
|
final auth = File('lib/shared/api/auth_api.dart').readAsStringSync();
|
|
final config = File('lib/shared/api/config_api.dart').readAsStringSync();
|
|
final recycle = File(
|
|
'lib/features/settings/recycle_bin_page.dart',
|
|
).readAsStringSync();
|
|
|
|
expect(auth, contains("'ai_companion_\${BackendIdentity.scope}_\$userId'"));
|
|
expect(config, contains("'public_avatars_\${BackendIdentity.scope}'"));
|
|
expect(config, contains("'public_personas_\${BackendIdentity.scope}'"));
|
|
expect(recycle, contains('availability != BackendAvailability.online'));
|
|
expect(recycle, contains('if (online)'));
|
|
});
|
|
}
|