feat(library): an empty library says it is on its way during the first sync
A device's first sync brings a person's whole library at its end, so an empty library said "No studysets yet" until then. Until a sync has gone through on this device for this person, it now says "Your library is on its way from <instance>…" with a spinner, unless the sync failed. Whether one has is kept beside the pending count in sync_pending.json (a file from before counts as synced), read as the person's data opens, and carried in SyncStatus.syncedHere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
bb7757d7f3
commit
952d2b0607
5 changed files with 237 additions and 30 deletions
|
|
@ -0,0 +1,40 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/sync/pending_changes_store.dart';
|
||||
|
||||
void main() {
|
||||
late Directory folder;
|
||||
late File file;
|
||||
|
||||
setUp(() {
|
||||
folder = Directory.systemTemp.createTempSync('sync_pending');
|
||||
file = File('${folder.path}/sync_pending.json');
|
||||
});
|
||||
tearDown(() => folder.deleteSync(recursive: true));
|
||||
|
||||
test('nothing kept yet: none pending, never synced here', () async {
|
||||
expect(await FilePendingChangesStore(file).read(), (
|
||||
pending: 0,
|
||||
synced: false,
|
||||
));
|
||||
});
|
||||
|
||||
test('keeps the count and whether a sync went through', () async {
|
||||
await FilePendingChangesStore(file).write((pending: 3, synced: true));
|
||||
|
||||
expect(await FilePendingChangesStore(file).read(), (
|
||||
pending: 3,
|
||||
synced: true,
|
||||
));
|
||||
});
|
||||
|
||||
test('a file from before it kept that is a device that synced', () async {
|
||||
file.writeAsStringSync('{"pending": 2}');
|
||||
|
||||
expect(await FilePendingChangesStore(file).read(), (
|
||||
pending: 2,
|
||||
synced: true,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
@ -20,7 +20,10 @@ void main() {
|
|||
// While set and not completed, a sync keeps running.
|
||||
late Completer<void>? hold;
|
||||
|
||||
SyncService service({PendingChangesStore? pendingChanges}) => SyncService(
|
||||
SyncService service({
|
||||
PendingChangesStore? pendingChanges,
|
||||
bool syncedBefore = false,
|
||||
}) => SyncService(
|
||||
({required onMerge}) async {
|
||||
syncs++;
|
||||
merge = onMerge;
|
||||
|
|
@ -30,6 +33,7 @@ void main() {
|
|||
}
|
||||
},
|
||||
pendingChanges: pendingChanges,
|
||||
syncedBefore: syncedBefore,
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
|
|
@ -182,7 +186,10 @@ void main() {
|
|||
async.flushMicrotasks();
|
||||
expect(
|
||||
sync.status,
|
||||
SyncStatus(lastSyncedAt: noon.add(const Duration(seconds: 3))),
|
||||
SyncStatus(
|
||||
lastSyncedAt: noon.add(const Duration(seconds: 3)),
|
||||
syncedHere: true,
|
||||
),
|
||||
);
|
||||
sync.dispose();
|
||||
});
|
||||
|
|
@ -205,6 +212,7 @@ void main() {
|
|||
at: noon.add(const Duration(minutes: 5)),
|
||||
appMismatch: false,
|
||||
),
|
||||
syncedHere: true,
|
||||
),
|
||||
);
|
||||
expect(sync.status.hasTrouble, isTrue);
|
||||
|
|
@ -213,7 +221,10 @@ void main() {
|
|||
async.elapse(const Duration(minutes: 5));
|
||||
expect(
|
||||
sync.status,
|
||||
SyncStatus(lastSyncedAt: noon.add(const Duration(minutes: 10))),
|
||||
SyncStatus(
|
||||
lastSyncedAt: noon.add(const Duration(minutes: 10)),
|
||||
syncedHere: true,
|
||||
),
|
||||
);
|
||||
sync.dispose();
|
||||
});
|
||||
|
|
@ -354,4 +365,30 @@ void main() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('says whether a sync ever went through on this device: as kept from '
|
||||
'before, and from the first that goes through, kept for next '
|
||||
'time', () {
|
||||
fakeAsync((async) {
|
||||
expect(service(syncedBefore: true).status.syncedHere, isTrue);
|
||||
|
||||
failWith = Exception('No connection');
|
||||
final kept = MemoryPendingChangesStore(0, false);
|
||||
final sync = service(pendingChanges: kept)..start();
|
||||
async.flushMicrotasks();
|
||||
expect(sync.status.syncedHere, isFalse);
|
||||
expect(sync.status.hasTrouble, isTrue);
|
||||
|
||||
failWith = null;
|
||||
sync.syncNow();
|
||||
async.flushMicrotasks();
|
||||
expect(sync.status.syncedHere, isTrue);
|
||||
expect(kept.synced, isTrue);
|
||||
|
||||
sync.requestSync();
|
||||
async.flushMicrotasks();
|
||||
expect(sync.status.syncedHere, isTrue);
|
||||
sync.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
||||
|
|
@ -114,6 +116,35 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets("until this device's first sync went through, an empty "
|
||||
'library says it is on its way; then that there are no studysets '
|
||||
'yet', (tester) async {
|
||||
tester.view.physicalSize = const Size(1200, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final first = Completer<void>();
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: gateway,
|
||||
syncedBefore: false,
|
||||
syncOnce: ({required onMerge}) => first.future,
|
||||
),
|
||||
);
|
||||
|
||||
// Its spinner turns without end: frames, not settling.
|
||||
await tester.tap(tab('Library'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(find.textContaining('Your library is on its way from'), findsOne);
|
||||
expect(find.text('No studysets yet'), findsNothing);
|
||||
|
||||
first.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('on its way'), findsNothing);
|
||||
expect(find.text('No studysets yet'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('says when there are no studysets yet', (tester) async {
|
||||
await showLibrary(tester);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
import 'package:halkyon_learn_flutter/src/core/sync/pending_changes_store.dart';
|
||||
|
||||
/// Keeps the count of changes not on the server yet in memory.
|
||||
class MemoryPendingChangesStore([int initial = 0])
|
||||
/// Keeps the count of changes not on the server yet, and whether a sync
|
||||
/// went through, in memory.
|
||||
class MemoryPendingChangesStore([int initial = 0, bool synced = true])
|
||||
implements PendingChangesStore {
|
||||
var _count = initial;
|
||||
var _synced = synced;
|
||||
|
||||
/// The count kept now.
|
||||
int get kept => _count;
|
||||
|
||||
@override
|
||||
Future<int> read() async => _count;
|
||||
/// Whether it keeps that a sync went through.
|
||||
bool get synced => _synced;
|
||||
|
||||
@override
|
||||
Future<void> write(int count) async => _count = count;
|
||||
Future<KeptSync> read() async => (pending: _count, synced: _synced);
|
||||
|
||||
@override
|
||||
Future<void> write(KeptSync kept) async {
|
||||
_count = kept.pending;
|
||||
_synced = kept.synced;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ Dependencies testDependencies({
|
|||
DiscussionGateway? discussionGateway,
|
||||
SyncOnce? syncOnce,
|
||||
PendingChangesStore? pendingChanges,
|
||||
// A device that synced before, as most are; false for one whose first
|
||||
// sync is still to go through.
|
||||
bool syncedBefore = true,
|
||||
OpenPerson? openPerson,
|
||||
AssistantHandOff? assistantHandOff,
|
||||
WebSearch? webSearch,
|
||||
|
|
@ -237,6 +240,7 @@ Dependencies testDependencies({
|
|||
final sync = SyncService(
|
||||
syncOnce ?? ({required onMerge}) async {},
|
||||
pendingChanges: pendingChanges,
|
||||
syncedBefore: syncedBefore,
|
||||
);
|
||||
if (syncOnce != null) {
|
||||
sync.syncNow();
|
||||
|
|
|
|||
Loading…
Reference in a new issue