Skip to content

Commit a79c15b

Browse files
committed
[red] cover recovery startup timeout and preserved live text
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent b7b0fed commit a79c15b

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: MIT
2+
// Part of PyBLE (https://pyble.dev) — see /LICENSE.
3+
4+
import 'dart:async';
5+
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_riverpod/flutter_riverpod.dart';
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
import 'package:pyble/data/draft_repository.dart';
11+
import 'package:pyble/editor/draft_recovery.dart';
12+
import 'package:pyble/editor/editor_document.dart';
13+
import 'package:pyble/editor/recovery_gate.dart';
14+
import 'package:pyble/editor/recovery_panel.dart';
15+
import 'package:pyble/pble/pble.dart';
16+
17+
import '../support/feature_harness.dart';
18+
19+
class _Store implements DraftRepository {
20+
DraftRead slot = DraftRead(generation: 1, snapshot: DraftSnapshot(
21+
name: 'main.py', content: 'prior text', originPath: '/main.py',
22+
generation: 1, checkpointTime: DateTime.utc(2026, 10, 31),
23+
));
24+
Completer<DraftRead>? delayed;
25+
bool failClear = false;
26+
@override
27+
Future<DraftRead> load() async => delayed == null ? slot : delayed!.future;
28+
@override
29+
Future<int> clear({required int expectedGeneration}) async {
30+
if (failClear) throw const DraftStorageException(DraftStorageFailure.unavailable);
31+
slot = DraftRead(generation: slot.generation + 1);
32+
return slot.generation;
33+
}
34+
@override
35+
Future<void> write(DraftSnapshot snapshot, {required int expectedGeneration}) async {
36+
slot = DraftRead(generation: snapshot.generation, snapshot: snapshot);
37+
}
38+
@override
39+
Future<void> close() async {}
40+
}
41+
42+
Future<void> _pump(WidgetTester tester, _Store store) async {
43+
final FakeConnection connection = FakeConnection();
44+
addTearDown(connection.dispose);
45+
await pumpSurface(tester, const RecoveryGate(
46+
child: Text('connection workflow mounted'),
47+
), connection: connection, extra: <Override>[
48+
draftRepositoryProvider.overrideWithValue(store),
49+
]);
50+
await tester.pump();
51+
}
52+
53+
void main() {
54+
testWidgets('startup resolves explicit local choice before mounting connection workflow',
55+
(WidgetTester tester) async {
56+
final _Store store = _Store();
57+
await _pump(tester, store);
58+
expect(find.text('connection workflow mounted'), findsNothing);
59+
expect(find.text('prior text'), findsOneWidget);
60+
expect(containerOf(tester).read(editorDocumentProvider).content, isEmpty);
61+
await tester.tap(find.byKey(kRecoveryRestoreKey));
62+
await tester.pumpAndSettle();
63+
expect(find.text('connection workflow mounted'), findsOneWidget);
64+
final EditorDocument restored = containerOf(tester).read(editorDocumentProvider);
65+
expect(restored.content, 'prior text');
66+
expect(restored.boardPath, isNull);
67+
expect(restored.dirty, isTrue);
68+
expect(store.slot.snapshot!.content, 'prior text');
69+
});
70+
71+
testWidgets('failed discard retains preview and confirmation choices',
72+
(WidgetTester tester) async {
73+
final _Store store = _Store()..failClear = true;
74+
await _pump(tester, store);
75+
await tester.tap(find.byKey(kRecoveryDiscardKey));
76+
await tester.pumpAndSettle();
77+
await tester.tap(find.byKey(kRecoveryConfirmDiscardKey));
78+
await tester.pumpAndSettle();
79+
expect(find.text('connection workflow mounted'), findsNothing);
80+
expect(find.text('prior text'), findsOneWidget);
81+
expect(find.byKey(kRecoveryRestoreKey), findsOneWidget);
82+
expect(store.slot.snapshot!.content, 'prior text');
83+
expect(find.textContaining('could not be read or written'), findsOneWidget);
84+
});
85+
86+
testWidgets('timeout offers explicit volatile session and ignores a late startup result',
87+
(WidgetTester tester) async {
88+
final _Store store = _Store()..delayed = Completer<DraftRead>();
89+
await _pump(tester, store);
90+
expect(find.text('connection workflow mounted'), findsNothing);
91+
await tester.pump(const Duration(seconds: 10));
92+
expect(find.byKey(kRecoveryRetryKey), findsOneWidget);
93+
expect(find.byKey(kRecoveryContinueKey), findsOneWidget);
94+
await tester.tap(find.byKey(kRecoveryContinueKey));
95+
await tester.pump();
96+
expect(find.text('connection workflow mounted'), findsOneWidget);
97+
expect(find.textContaining('Recovery is off'), findsOneWidget);
98+
containerOf(tester).read(editorDocumentProvider.notifier).setContent('live text');
99+
store.delayed!.complete(store.slot);
100+
await tester.pump();
101+
expect(containerOf(tester).read(editorDocumentProvider).content, 'live text');
102+
expect(find.text('prior text'), findsNothing);
103+
});
104+
105+
testWidgets('retry previews both live text and stored draft before explicit replacement',
106+
(WidgetTester tester) async {
107+
final _Store store = _Store()..delayed = Completer<DraftRead>();
108+
await _pump(tester, store);
109+
await tester.pump(const Duration(seconds: 10));
110+
await tester.tap(find.byKey(kRecoveryContinueKey));
111+
await tester.pump();
112+
containerOf(tester).read(editorDocumentProvider.notifier).setContent('live text');
113+
store.delayed!.complete(store.slot);
114+
await tester.pump();
115+
store.delayed = null;
116+
await tester.tap(find.byKey(kRecoveryRetryKey));
117+
await tester.pump();
118+
await tester.pump();
119+
expect(find.text('live text'), findsOneWidget);
120+
expect(find.text('prior text'), findsOneWidget);
121+
expect(containerOf(tester).read(editorDocumentProvider).content, 'live text');
122+
});
123+
}

0 commit comments

Comments
 (0)