import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart' show sha256; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:observer/camera/model_catalog_section.dart'; import 'package:observer/models/model_manager.dart'; const _modelBytes = [1, 2, 3, 4, 5, 6, 7, 8]; String _shaHex(List bytes) => sha256.convert(bytes).toString(); /// 分两段到达的下载流(中间 50ms 停顿,供取消测试在下载中触发) Stream> _delayedChunks() async* { yield [1, 2, 3, 4]; await Future.delayed(const Duration(milliseconds: 50)); yield [5, 6, 7, 8]; } Map _item() => { 'datasetId': 7, 'datasetName': '环颈雉鸡数据集', 'version': 'v1.0.0', 'labels': ['pheasant', 'suspect'], 'sizeBytes': _modelBytes.length, 'sha256': _shaHex(_modelBytes), 'downloadUrl': '/download/models/7/latest.tflite', 'coverUrl': '/api/v1/app/cover?namePrefix=RNPHE', }; void main() { late Directory root; setUp(() async { root = await Directory.systemTemp.createTemp('catalog-section-test'); }); tearDown(() => root.delete(recursive: true)); ModelManager manager() => ModelManager( baseUrl: 'http://test.local', client: MockClient((req) async { if (req.url.path == '/api/v1/app/update') { return http.Response.bytes( utf8.encode(jsonEncode({ 'code': 0, 'message': 'ok', 'data': {'models': [_item()]} })), 200); } if (req.url.path.startsWith('/download/models/')) { return http.Response.bytes(_modelBytes, 200); } return http.Response('not found', 404); }), rootDir: () async => root, ); Future pumpSection(WidgetTester tester, ModelManager m) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: ModelCatalogSection(manager: m)))); await tester.pumpAndSettle(); } testWidgets('未下载:显示使用按钮,点击后下载完成自动变为已使用', (tester) async { final m = manager(); await m.refresh(); await pumpSection(tester, m); expect(find.text('环颈雉鸡数据集'), findsOneWidget); expect(find.text('使用'), findsOneWidget); await tester.tap(find.text('使用')); await tester.pumpAndSettle(); expect(m.isDownloaded(7), isTrue); expect(m.isActive(7), isTrue); expect(find.text('已使用'), findsOneWidget); }); testWidgets('已激活:再次点击取消使用', (tester) async { final m = manager(); await m.refresh(); await m.downloadModel(m.catalog.first); await pumpSection(tester, m); expect(find.text('已使用'), findsOneWidget); await tester.tap(find.text('已使用')); await tester.pumpAndSettle(); expect(m.isActive(7), isFalse); expect(find.text('使用'), findsOneWidget); }); testWidgets('目录为空:显示暂无已发布模型', (tester) async { final m = ModelManager( baseUrl: 'http://test.local', client: MockClient((_) async => http.Response.bytes( utf8.encode(jsonEncode( {'code': 0, 'message': 'ok', 'data': {'models': []}})), 200)), rootDir: () async => root, ); await m.refresh(); await pumpSection(tester, m); expect(find.text('暂无已发布模型'), findsOneWidget); }); testWidgets('下载中:显示进度和取消按钮,取消后恢复使用', (tester) async { final m = ModelManager( baseUrl: 'http://test.local', client: MockClient((req) async { if (req.url.path == '/api/v1/app/update') { return http.Response.bytes( utf8.encode(jsonEncode({ 'code': 0, 'message': 'ok', 'data': {'models': [_item()]} })), 200); } if (req.url.path.startsWith('/download/models/')) { return http.Response.fromStream(http.StreamedResponse( _delayedChunks(), 200, contentLength: _modelBytes.length)); } return http.Response('not found', 404); }), rootDir: () async => root, ); await m.refresh(); await pumpSection(tester, m); await tester.tap(find.text('使用')); await tester.pump(); // 首个分块到达,进度条与取消按钮出现 expect(find.text('取消'), findsOneWidget); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); expect(m.isDownloaded(7), isFalse); expect(m.isActive(7), isFalse); expect(find.text('使用'), findsOneWidget); }); }