@@ -75,8 +75,10 @@ final class _FakeGithubApi implements GithubApi {
7575 'blink.py' : 'print(1)\n ' ,
7676 },
7777 this .resolveFailure,
78+ this .childListFailure,
7879 this .resolveGate,
7980 this .listGate,
81+ this .fetchFailuresRemaining = 0 ,
8082 }) : _bytesByRemotePath = < String , Uint8List > {
8183 for (final MapEntry <String , String > source
8284 in sourceByRemotePath.entries)
@@ -86,8 +88,10 @@ final class _FakeGithubApi implements GithubApi {
8688 final List <GithubEntry > entries;
8789 final Map <String , Uint8List > _bytesByRemotePath;
8890 final GithubFailure ? resolveFailure;
91+ final GithubFailure ? childListFailure;
8992 final Completer <void >? resolveGate;
9093 final Completer <void >? listGate;
94+ int fetchFailuresRemaining;
9195 final List <(RepositoryLocator , String )> resolveCalls =
9296 < (RepositoryLocator , String )> [];
9397 final List <String > listedRemotePaths = < String > [];
@@ -97,6 +101,7 @@ final class _FakeGithubApi implements GithubApi {
97101 Future <PinnedRepository > resolve (
98102 RepositoryLocator locator, {
99103 String ref = '' ,
104+ GithubCancellation ? cancellation,
100105 }) async {
101106 resolveCalls.add ((locator, ref));
102107 await resolveGate? .future;
@@ -115,9 +120,14 @@ final class _FakeGithubApi implements GithubApi {
115120 PinnedRepository repository, {
116121 required String treeSha,
117122 required String remotePath,
123+ GithubCancellation ? cancellation,
118124 }) async {
119125 listedRemotePaths.add (remotePath);
120126 await listGate? .future;
127+ if (remotePath.isNotEmpty && childListFailure
128+ case final GithubFailure failure) {
129+ throw failure;
130+ }
121131 return GithubDirectory (
122132 treeSha: treeSha,
123133 remotePath: remotePath,
@@ -128,9 +138,14 @@ final class _FakeGithubApi implements GithubApi {
128138 @override
129139 Future <Uint8List > fetchFile (
130140 PinnedRepository repository,
131- GithubEntry entry,
132- ) async {
141+ GithubEntry entry, {
142+ GithubCancellation ? cancellation,
143+ }) async {
133144 fetchedRemotePaths.add (entry.remotePath);
145+ if (fetchFailuresRemaining > 0 ) {
146+ fetchFailuresRemaining -= 1 ;
147+ throw GithubFailure (GithubFailureKind .offline, path: entry.remotePath);
148+ }
134149 return Uint8List .fromList (_bytesByRemotePath[entry.remotePath]! );
135150 }
136151}
@@ -566,5 +581,96 @@ void main() {
566581 findsOneWidget,
567582 );
568583 });
584+
585+ testWidgets ('failed child navigation retains the parent selection' , (
586+ WidgetTester tester,
587+ ) async {
588+ final RecordingConnection connection = RecordingConnection (
589+ initial: ConnState .ready,
590+ );
591+ final _FakeGithubApi api = _FakeGithubApi (
592+ childListFailure: const GithubFailure (GithubFailureKind .server),
593+ );
594+ addTearDown (connection.dispose);
595+
596+ await _openImport (tester, connection, api);
597+ await _browse (tester);
598+ await tester.tap (find.byKey (_selectionKey ('hello.py' )));
599+ await tester.pump ();
600+ await tester.tap (find.byKey (_entryKey ('nested' )));
601+ await tester.pumpAndSettle ();
602+
603+ expect (
604+ tester
605+ .widget <CheckboxListTile >(find.byKey (_selectionKey ('hello.py' )))
606+ .value,
607+ isTrue,
608+ );
609+ expect (find.text ('/' ), findsOneWidget);
610+ });
611+
612+ testWidgets ('a zero-write blob failure retries from the same review' , (
613+ WidgetTester tester,
614+ ) async {
615+ final RecordingConnection connection = RecordingConnection (
616+ initial: ConnState .ready,
617+ );
618+ final _FakeGithubApi api = _FakeGithubApi (
619+ entries: const < GithubEntry > [_helloEntry],
620+ fetchFailuresRemaining: 1 ,
621+ );
622+ addTearDown (connection.dispose);
623+
624+ await _openImport (tester, connection, api);
625+ await _browse (tester);
626+ await tester.tap (find.byKey (_selectionKey ('hello.py' )));
627+ await tester.pump ();
628+ await tester.tap (find.byKey (kGithubReviewButtonKey));
629+ await tester.pumpAndSettle ();
630+ await tester.tap (find.byKey (kGithubCommitButtonKey));
631+ await tester.pumpAndSettle ();
632+
633+ expect (connection.putFileCalls, isEmpty);
634+ expect (find.text (l10nOf (tester).githubImportRetry), findsOneWidget);
635+ expect (find.byKey (kGithubCommitButtonKey), findsOneWidget);
636+
637+ await tester.tap (find.text (l10nOf (tester).githubImportRetry));
638+ await tester.pumpAndSettle ();
639+
640+ expect (connection.putFileCalls, hasLength (1 ));
641+ expect (
642+ find.byKey (const ValueKey <String >('githubImportResult' )),
643+ findsOneWidget,
644+ );
645+ });
646+
647+ testWidgets ('review marks an existing regular-file target before consent' , (
648+ WidgetTester tester,
649+ ) async {
650+ final RecordingConnection connection = RecordingConnection (
651+ initial: ConnState .ready,
652+ );
653+ final _FakeGithubApi api = _FakeGithubApi (
654+ entries: const < GithubEntry > [_helloEntry],
655+ );
656+ addTearDown (connection.dispose);
657+ await connection.putFile (
658+ '/hello.py' ,
659+ Uint8List .fromList (utf8.encode ('print(0)\n ' )),
660+ );
661+
662+ await _openImport (tester, connection, api);
663+ await _browse (tester);
664+ await tester.tap (find.byKey (_selectionKey ('hello.py' )));
665+ await tester.pump ();
666+ await tester.tap (find.byKey (kGithubReviewButtonKey));
667+ await tester.pumpAndSettle ();
668+
669+ expect (
670+ find.text (l10nOf (tester).githubImportWillOverwrite),
671+ findsOneWidget,
672+ );
673+ expect (find.textContaining ('/hello.py' ), findsOneWidget);
674+ });
569675 });
570676}
0 commit comments