import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:yx_tracking_flutter/src/config/config_manager.dart'; import 'package:yx_tracking_flutter/src/core/analytics_core.dart'; import 'package:yx_tracking_flutter/src/core/scheduler.dart'; import 'package:yx_tracking_flutter/src/model/system_dim_info.dart'; import 'package:yx_tracking_flutter/src/network/api_client.dart'; import 'package:yx_tracking_flutter/src/storage/config_storage.dart'; import 'package:yx_tracking_flutter/src/storage/event_storage.dart'; import 'package:yx_tracking_flutter/yx_tracking_flutter.dart'; class _NoopInterceptor implements AnalyticsInterceptor { @override FutureOr beforeSend(Event event) async => event; @override FutureOr afterSend(Event event, SendResult result) async {} } class _MemoryEventStorage implements EventStorage { final _items = []; var _nextId = 1; @override Future init() async {} @override Future insert(Event event) async { final stored = StoredEvent( id: _nextId, event: event, createTime: event.createTime, retryCount: event.retryCount, ); _items.add(stored); _nextId += 1; return stored.id; } @override Future> fetchBatch(int limit) async { if (limit <= 0) return const []; final copy = List.from(_items) ..sort((a, b) => a.createTime.compareTo(b.createTime)); return copy.take(limit).toList(growable: false); } @override Future> fetchRecent(int limit) async { if (limit <= 0) return const []; final copy = List.from(_items) ..sort((a, b) => b.createTime.compareTo(a.createTime)); return copy.take(limit).toList(growable: false); } @override Future deleteByIds(List ids) async { if (ids.isEmpty) return; _items.removeWhere((e) => ids.contains(e.id)); } @override Future count() async => _items.length; @override Future trimToMaxSize(int maxSize) async { if (maxSize <= 0) { final removed = _items.length; _items.clear(); return removed; } final overflow = _items.length - maxSize; if (overflow <= 0) return 0; _items ..sort((a, b) => a.createTime.compareTo(b.createTime)) ..removeRange(0, overflow); return overflow; } @override Future deleteExpired(DateTime cutoff) async { final before = _items.length; _items.removeWhere((e) => !e.createTime.isAfter(cutoff)); return before - _items.length; } @override Future updateRetryCount(int id, int retryCount) async { final index = _items.indexWhere((e) => e.id == id); if (index < 0) return; final current = _items[index]; _items[index] = current.copyWith( retryCount: retryCount, event: current.event.copyWith(retryCount: retryCount), ); } @override Future dispose() async {} } class _FakeApiClient extends ApiClient { _FakeApiClient(super.config); int sent = 0; @override Future sendBatch(List events) async { sent += events.length; } } class _MemoryConfigStorage implements ConfigStorage { SystemDimInfo? _value; @override Future init() async {} @override Future saveSystemDimInfo(SystemDimInfo info) async { _value = info; } @override Future loadSystemDimInfo() async => _value; @override Future clear() async { _value = null; } @override Future dispose() async {} } class _TestConfigManager extends ConfigManager { _TestConfigManager({required super.config}) : super( storage: _MemoryConfigStorage(), refreshInterval: Duration.zero, httpClient: null, ); SystemDimInfo? _current; @override SystemDimInfo? get currentConfig => _current; @override Future init() async { _current ??= SystemDimInfo( systemInfo: const SystemInfo(raw: {}), eventDefinitions: const [ EventDefinition(eventCode: 'FACADE_EVENT'), EventDefinition(eventCode: 'SDK_METRICS_SEND'), EventDefinition(eventCode: 'SDK_METRICS_QUEUE'), ], tagDefinitions: const [], sdkStrategy: const SdkStrategy( enabled: true, defaultSampleRate: 1, eventSettings: {}, ), lastFetchedAt: DateTime.fromMillisecondsSinceEpoch(1), ); } @override Future fetchAndCacheConfig({bool force = false}) async {} @override Future forceRefresh() async {} @override Future dispose() async {} } class _NoopScheduler extends Scheduler { _NoopScheduler({required super.interval, required super.onTick}); @override void start() {} @override void stop() {} } AnalyticsConfig _config() { return AnalyticsConfig( systemCode: 'TEST_APP', endpointBaseUrl: 'https://example.com', clientType: 3, enableDebug: true, metricsReportInterval: Duration(days: 1), ); } void main() { group('Analytics Facade', () { test('可注入 core 并走通关键路径', () async { final memoryStorage = _MemoryEventStorage(); final fakeApiClient = _FakeApiClient(_config()); final core = AnalyticsCore( storageFactory: () => memoryStorage, apiClientFactory: (_) => fakeApiClient, configManagerFactory: (config) => _TestConfigManager(config: config), deviceInfoCollector: () async => const DeviceInfo( os: 'test-os', model: 'test-model', screenResolution: '100x200', ), schedulerFactory: (interval, onTick) => _NoopScheduler(interval: interval, onTick: onTick), randomDouble: () => 0, now: () => DateTime.fromMillisecondsSinceEpoch(10), ); Analytics.coreForTesting = core; expect(Analytics.instance(), isA()); expect(Analytics.coreForTesting, same(core)); await Analytics.init(_config()); await Analytics.setUser(const UserInfo(userId: 1, userName: 'u')); await Analytics.setDeviceInfo( const DeviceInfo(os: 'o', model: 'm', screenResolution: '1x1'), ); await Analytics.track( 'FACADE_EVENT', eventParams: const {'k': 1}, ); expect(await Analytics.cachedEventCount(), greaterThanOrEqualTo(1)); expect(await Analytics.cachedRecentEvents(limit: 5), isNotEmpty); await Analytics.flush(force: true); await Analytics.refreshConfig(force: false); await Analytics.reportMetricsNow(); Analytics.addInterceptor(_NoopInterceptor()); Analytics.setDebug(enabled: true); expect(fakeApiClient.sent, greaterThanOrEqualTo(1)); await Analytics.dispose(); }); }); }