yx_tracking_flutter/test/facade_test.dart

249 lines
6.7 KiB
Dart

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<Event?> beforeSend(Event event) async => event;
@override
FutureOr<void> afterSend(Event event, SendResult result) async {}
}
class _MemoryEventStorage implements EventStorage {
final _items = <StoredEvent>[];
var _nextId = 1;
@override
Future<void> init() async {}
@override
Future<int> 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<List<StoredEvent>> fetchBatch(int limit) async {
if (limit <= 0) return const <StoredEvent>[];
final copy = List<StoredEvent>.from(_items)
..sort((a, b) => a.createTime.compareTo(b.createTime));
return copy.take(limit).toList(growable: false);
}
@override
Future<List<StoredEvent>> fetchRecent(int limit) async {
if (limit <= 0) return const <StoredEvent>[];
final copy = List<StoredEvent>.from(_items)
..sort((a, b) => b.createTime.compareTo(a.createTime));
return copy.take(limit).toList(growable: false);
}
@override
Future<void> deleteByIds(List<int> ids) async {
if (ids.isEmpty) return;
_items.removeWhere((e) => ids.contains(e.id));
}
@override
Future<int> count() async => _items.length;
@override
Future<int> 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<int> deleteExpired(DateTime cutoff) async {
final before = _items.length;
_items.removeWhere((e) => !e.createTime.isAfter(cutoff));
return before - _items.length;
}
@override
Future<void> 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<void> dispose() async {}
}
class _FakeApiClient extends ApiClient {
_FakeApiClient(super.config);
int sent = 0;
@override
Future<void> sendBatch(List<Event> events) async {
sent += events.length;
}
}
class _MemoryConfigStorage implements ConfigStorage {
SystemDimInfo? _value;
@override
Future<void> init() async {}
@override
Future<void> saveSystemDimInfo(SystemDimInfo info) async {
_value = info;
}
@override
Future<SystemDimInfo?> loadSystemDimInfo() async => _value;
@override
Future<void> clear() async {
_value = null;
}
@override
Future<void> 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<void> init() async {
_current ??= SystemDimInfo(
systemInfo: const SystemInfo(raw: <String, Object?>{}),
eventDefinitions: const <EventDefinition>[
EventDefinition(eventCode: 'FACADE_EVENT'),
EventDefinition(eventCode: 'SDK_METRICS_SEND'),
EventDefinition(eventCode: 'SDK_METRICS_QUEUE'),
],
tagDefinitions: const <TagDefinition>[],
sdkStrategy: const SdkStrategy(
enabled: true,
defaultSampleRate: 1,
eventSettings: <String, EventStrategy>{},
),
lastFetchedAt: DateTime.fromMillisecondsSinceEpoch(1),
);
}
@override
Future<void> fetchAndCacheConfig({bool force = false}) async {}
@override
Future<void> forceRefresh() async {}
@override
Future<void> 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',
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<Analytics>());
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 <String, Object?>{'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();
});
});
}