mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 21:27:01 +01:00
Every split is by test-group/concern, with the shared setup moved to a
sibling helper the splits import. All 313 tests are preserved -- the count
is unchanged and `test('…')` names are byte-identical to the originals
(verified by diffing the sorted name lists).
note_repository_test 677 -> 4 files + note_repository_helpers
capture_screen_test 660 -> 4 files + capture_screen_harness
note_editor_test 502 -> 3 files + note_editor_harness
port_guard_test 405 -> 3 files + port_guard_fakes
wrapper_server_test 402 -> 2 files + wrapper_server_harness
settings_screen_test 391 -> 2 files + settings_screen_harness
github_mirror_screen_test 365 -> 2 files + github_mirror_harness
notes_list_screen_test 347 -> 2 files + notes_list_harness
analytics_service_test 300 -> 2 files + analytics_helpers
Helpers are deliberately NOT named `*_test.dart`; the runner would collect
them and fail on the missing `main()`.
Three things did not survive a mechanical split and were handled explicitly:
* wrapper_server's `enabledOrigin` closes over the `late root` that setUp
assigns, so a top-level function cannot see it. It is now a
WrapperHarness class with an install() that registers the setUp/tearDown
pair -- the only structural change in this commit.
* note_repository's splits each repeat `setUpAll(sqfliteFfiInit)`, and
analytics' each repeat `@TestOn('vm')`. Omitting either fails quietly
(uninitialised sqflite / wrong platform) rather than loudly.
* capture_screen's `recordingMock` was declared mid-file between two
tests, so it moved to the harness rather than being dropped.
Verified: 313 tests pass (same as before the refactor), 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
4.5 KiB
Dart
140 lines
4.5 KiB
Dart
/// Tests for the interaction-only analytics buffer and Firebase flush.
|
|
@TestOn('vm')
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart' as http_testing;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:todo/analytics/analytics_event.dart';
|
|
import 'package:todo/analytics/analytics_service.dart';
|
|
|
|
import 'analytics_helpers.dart';
|
|
|
|
FirebaseRestClient _fakeClient(
|
|
Future<http.Response> Function(http.Request request) handler,
|
|
) => FirebaseRestClient(
|
|
databaseUrl: 'https://x-rtdb.europe-west1.firebasedatabase.app',
|
|
auth: FirebaseTokenProvider(
|
|
apiKey: 'AIzaKey',
|
|
store: InMemoryCredentialStore(
|
|
FirebaseCredentials(
|
|
idToken: 'id',
|
|
refreshToken: 'refresh',
|
|
expiresAt: DateTime.now().add(const Duration(hours: 1)),
|
|
),
|
|
),
|
|
),
|
|
httpClient: http_testing.MockClient(handler),
|
|
);
|
|
|
|
void main() {
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
|
|
group('AnalyticsEvent', () {
|
|
test('round-trips through toJson/tryFromJson', () {
|
|
final event = AnalyticsEvent(
|
|
name: 'screen_view',
|
|
timestamp: DateTime(2026, 1, 1, 12),
|
|
params: {'screen': 'capture'},
|
|
);
|
|
final decoded = AnalyticsEvent.tryFromJson(event.toJson());
|
|
|
|
expect(decoded, isNotNull);
|
|
expect(decoded!.name, 'screen_view');
|
|
expect(decoded.timestamp, event.timestamp);
|
|
expect(decoded.params, {'screen': 'capture'});
|
|
});
|
|
|
|
test('tryFromJson returns null for a non-map value', () {
|
|
expect(AnalyticsEvent.tryFromJson('not a map'), isNull);
|
|
expect(AnalyticsEvent.tryFromJson(null), isNull);
|
|
});
|
|
|
|
test('tryFromJson returns null for a missing/invalid name', () {
|
|
expect(
|
|
AnalyticsEvent.tryFromJson({'timestamp': '2026-01-01T00:00:00.000'}),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('tryFromJson returns null for an unparsable timestamp', () {
|
|
expect(
|
|
AnalyticsEvent.tryFromJson({'name': 'x', 'timestamp': 'not a date'}),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('tryFromJson defaults params to empty when missing/non-map', () {
|
|
final decoded = AnalyticsEvent.tryFromJson({
|
|
'name': 'x',
|
|
'timestamp': DateTime(2026).toIso8601String(),
|
|
});
|
|
expect(decoded!.params, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('AnalyticsService.logEvent', () {
|
|
test('appends to an initially empty buffer', () async {
|
|
const service = AnalyticsService(nodeId: 'device-a');
|
|
await service.logEvent(
|
|
AnalyticsEvent(name: 'app_open', timestamp: DateTime.now()),
|
|
);
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final raw = prefs.getString('analytics.buffer');
|
|
expect(raw, isNotNull);
|
|
final decoded = jsonDecode(raw!) as List;
|
|
expect(decoded, hasLength(1));
|
|
});
|
|
|
|
test('drops the oldest event once past the cap', () async {
|
|
const service = AnalyticsService(nodeId: 'device-a');
|
|
for (var i = 0; i < AnalyticsService.maxBufferedEvents + 5; i++) {
|
|
await service.logEvent(
|
|
AnalyticsEvent(
|
|
name: 'event_$i',
|
|
timestamp: DateTime.now(),
|
|
),
|
|
);
|
|
}
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final decoded = jsonDecode(prefs.getString('analytics.buffer')!) as List;
|
|
expect(decoded, hasLength(AnalyticsService.maxBufferedEvents));
|
|
// The oldest 5 events were dropped, so event_0..event_4 are gone.
|
|
final names = decoded.map((e) => (e as Map)['name']).toList();
|
|
expect(names, isNot(contains('event_0')));
|
|
expect(
|
|
names,
|
|
contains('event_${AnalyticsService.maxBufferedEvents + 4}'),
|
|
);
|
|
});
|
|
|
|
test('ignores a malformed pre-existing buffer entry on read', () async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'analytics.buffer': jsonEncode([
|
|
{'not': 'an event'},
|
|
{
|
|
'name': 'ok_event',
|
|
'timestamp': DateTime(2026).toIso8601String(),
|
|
'params': <String, Object?>{},
|
|
},
|
|
]),
|
|
});
|
|
const service = AnalyticsService(nodeId: 'device-a');
|
|
await service.logEvent(
|
|
AnalyticsEvent(name: 'new_event', timestamp: DateTime.now()),
|
|
);
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final decoded = jsonDecode(prefs.getString('analytics.buffer')!) as List;
|
|
// The malformed entry was dropped; only the valid + new event remain.
|
|
expect(decoded, hasLength(2));
|
|
});
|
|
});
|
|
}
|