|
| 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