77// Connection seam; no GitHub account, radio, or proprietary implementation is
88// involved.
99
10+ import 'dart:async' ;
1011import 'dart:convert' ;
1112import 'dart:typed_data' ;
1213
@@ -73,6 +74,9 @@ final class _FakeGithubApi implements GithubApi {
7374 'hello.py' : "print('hello')\n " ,
7475 'blink.py' : 'print(1)\n ' ,
7576 },
77+ this .resolveFailure,
78+ this .resolveGate,
79+ this .listGate,
7680 }) : _bytesByRemotePath = < String , Uint8List > {
7781 for (final MapEntry <String , String > source
7882 in sourceByRemotePath.entries)
@@ -81,6 +85,9 @@ final class _FakeGithubApi implements GithubApi {
8185
8286 final List <GithubEntry > entries;
8387 final Map <String , Uint8List > _bytesByRemotePath;
88+ final GithubFailure ? resolveFailure;
89+ final Completer <void >? resolveGate;
90+ final Completer <void >? listGate;
8491 final List <(RepositoryLocator , String )> resolveCalls =
8592 < (RepositoryLocator , String )> [];
8693 final List <String > listedRemotePaths = < String > [];
@@ -92,6 +99,8 @@ final class _FakeGithubApi implements GithubApi {
9299 String ref = '' ,
93100 }) async {
94101 resolveCalls.add ((locator, ref));
102+ await resolveGate? .future;
103+ if (resolveFailure case final GithubFailure failure) throw failure;
95104 return PinnedRepository (
96105 locator: locator,
97106 requestedRef: ref,
@@ -108,6 +117,7 @@ final class _FakeGithubApi implements GithubApi {
108117 required String remotePath,
109118 }) async {
110119 listedRemotePaths.add (remotePath);
120+ await listGate? .future;
111121 return GithubDirectory (
112122 treeSha: treeSha,
113123 remotePath: remotePath,
@@ -125,6 +135,25 @@ final class _FakeGithubApi implements GithubApi {
125135 }
126136}
127137
138+ final class _FailingPutConnection extends RecordingConnection {
139+ _FailingPutConnection () : super (initial: ConnState .ready);
140+
141+ int _putAttempt = 0 ;
142+
143+ @override
144+ Future <void > putFile (
145+ String path,
146+ Uint8List bytes, {
147+ ProgressCb ? onProgress,
148+ }) async {
149+ _putAttempt += 1 ;
150+ if (_putAttempt == 2 ) {
151+ injectError (const EIo ('scripted second PUT failure' ));
152+ }
153+ await super .putFile (path, bytes, onProgress: onProgress);
154+ }
155+ }
156+
128157Key _entryKey (String remotePath) => ValueKey <String >('githubEntry_$remotePath ' );
129158
130159Key _selectionKey (String remotePath) =>
@@ -308,5 +337,144 @@ void main() {
308337 expect (connection.runSourceCalls, isEmpty);
309338 },
310339 );
340+
341+ testWidgets (
342+ 'resolve, folder loading, rate guidance, and error focus are explicit' ,
343+ (WidgetTester tester) async {
344+ final RecordingConnection connection = RecordingConnection (
345+ initial: ConnState .ready,
346+ );
347+ final Completer <void > resolveGate = Completer <void >();
348+ final Completer <void > listGate = Completer <void >();
349+ final _FakeGithubApi loadingApi = _FakeGithubApi (
350+ resolveGate: resolveGate,
351+ listGate: listGate,
352+ );
353+ addTearDown (connection.dispose);
354+
355+ await _openImport (tester, connection, loadingApi);
356+ await tester.enterText (
357+ find.byKey (kGithubRepositoryFieldKey),
358+ 'https://github.com/PyBLE-dev/examples' ,
359+ );
360+ await tester.tap (find.byKey (kGithubBrowseButtonKey));
361+ await tester.pump ();
362+ expect (find.text ('Resolving repository…' ), findsOneWidget);
363+
364+ resolveGate.complete ();
365+ await tester.pump ();
366+ expect (find.text ('Loading repository folder…' ), findsOneWidget);
367+ listGate.complete ();
368+ await tester.pumpAndSettle ();
369+
370+ await tester.tap (find.byTooltip (l10nOf (tester).githubImportClose));
371+ await tester.pumpAndSettle ();
372+
373+ final _FakeGithubApi rateLimitedApi = _FakeGithubApi (
374+ resolveFailure: const GithubFailure (
375+ GithubFailureKind .rateLimited,
376+ rateLimitRemaining: 0 ,
377+ retryAfter: Duration (seconds: 120 ),
378+ ),
379+ );
380+ await _openImport (tester, connection, rateLimitedApi);
381+ await _browse (tester);
382+
383+ expect (find.textContaining ('120' ), findsOneWidget);
384+ final Finder failure = find.byKey (
385+ const ValueKey <String >('githubImportFailure' ),
386+ );
387+ expect (failure, findsOneWidget);
388+ expect (tester.widget <Focus >(failure).focusNode? .hasFocus, isTrue);
389+ expect (
390+ tester.getSemantics (failure),
391+ matchesSemantics (isLiveRegion: true ),
392+ );
393+ },
394+ );
395+
396+ testWidgets ('dismissal returns focus to the invoking Files action' , (
397+ WidgetTester tester,
398+ ) async {
399+ final RecordingConnection connection = RecordingConnection (
400+ initial: ConnState .ready,
401+ );
402+ final _FakeGithubApi api = _FakeGithubApi ();
403+ addTearDown (connection.dispose);
404+ await pumpSurface (
405+ tester,
406+ const FilesView (),
407+ connection: connection,
408+ extra: < Override > [githubApiProvider.overrideWithValue (api)],
409+ );
410+ await tester.pumpAndSettle ();
411+
412+ final Finder tooltip = find.byTooltip (l10nOf (tester).githubImportAction);
413+ final Finder buttonFinder = find.ancestor (
414+ of: tooltip,
415+ matching: find.byType (IconButton ),
416+ );
417+ final FocusNode ? invokingFocus = tester
418+ .widget <IconButton >(buttonFinder)
419+ .focusNode;
420+ expect (invokingFocus, isNotNull);
421+
422+ await tester.tap (tooltip);
423+ await tester.pumpAndSettle ();
424+ await tester.tap (find.byTooltip (l10nOf (tester).githubImportClose));
425+ await tester.pumpAndSettle ();
426+
427+ expect (invokingFocus! .hasFocus, isTrue);
428+ });
429+
430+ testWidgets (
431+ 'partial result labels succeeded, failed, and unattempted paths and announces once' ,
432+ (WidgetTester tester) async {
433+ final _FailingPutConnection connection = _FailingPutConnection ();
434+ final GithubEntry third = GithubEntry (
435+ name: 'third.py' ,
436+ remotePath: 'third.py' ,
437+ kind: GithubEntryKind .regularFile,
438+ objectSha: '7777777777777777777777777777777777777777' ,
439+ declaredSize: 9 ,
440+ );
441+ final _FakeGithubApi api = _FakeGithubApi (
442+ entries: < GithubEntry > [_helloEntry, _blinkEntry, third],
443+ sourceByRemotePath: const < String , String > {
444+ 'hello.py' : "print('hello')\n " ,
445+ 'blink.py' : 'print(1)\n ' ,
446+ 'third.py' : 'print(3)\n ' ,
447+ },
448+ );
449+ addTearDown (connection.dispose);
450+
451+ await _openImport (tester, connection, api);
452+ await _browse (tester);
453+ for (final String path in < String > [
454+ 'hello.py' ,
455+ 'blink.py' ,
456+ 'third.py' ,
457+ ]) {
458+ await tester.tap (find.byKey (_selectionKey (path)));
459+ }
460+ await tester.pump ();
461+ await tester.tap (find.byKey (kGithubReviewButtonKey));
462+ await tester.pumpAndSettle ();
463+ await tester.tap (find.byKey (kGithubCommitButtonKey));
464+ await tester.pumpAndSettle ();
465+
466+ expect (find.text ('Downloaded' ), findsOneWidget);
467+ expect (find.text ('Failed' ), findsOneWidget);
468+ expect (find.text ('Not attempted' ), findsOneWidget);
469+ final Finder result = find.byKey (
470+ const ValueKey <String >('githubImportResult' ),
471+ );
472+ expect (result, findsOneWidget);
473+ expect (
474+ tester.getSemantics (result),
475+ matchesSemantics (isLiveRegion: true ),
476+ );
477+ },
478+ );
311479 });
312480}
0 commit comments