Files
jizhi/frontend/test/offline_local_first_test.dart
T

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)'));
});
}