From 13fe4ffa1391183d60eb2b5683e65344523cca32 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 14:23:39 +0200 Subject: [PATCH 01/10] ci: fix nightly builds --- .../firebase_auth/firebase_auth_e2e_test.dart | 3 ++- .../firebase_database/data_snapshot_e2e.dart | 8 ++++++-- .../firebase_database/database_reference_e2e.dart | 1 + tests/integration_test/firebase_database/query_e2e.dart | 5 ++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart index 7693e8ce69d4..cca5cee5990c 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart @@ -32,9 +32,10 @@ void main() { }); setUp(() async { + await ensureSignedOut(); + // Reset users on emulator. await emulatorClearAllUsers(); - await ensureSignedOut(); try { await FirebaseAuth.instance.createUserWithEmailAndPassword( diff --git a/tests/integration_test/firebase_database/data_snapshot_e2e.dart b/tests/integration_test/firebase_database/data_snapshot_e2e.dart index 29ff36f4cd7d..8828ba723198 100644 --- a/tests/integration_test/firebase_database/data_snapshot_e2e.dart +++ b/tests/integration_test/firebase_database/data_snapshot_e2e.dart @@ -9,11 +9,15 @@ void setupDataSnapshotTests() { group('DataSnapshot', () { late DatabaseReference ref; DatabaseReference getRef(String path) { - return FirebaseDatabase.instance.ref('tests').child(path); + return ref.child(path); } + var testCount = 0; + setUp(() async { - ref = FirebaseDatabase.instance.ref('tests'); + ref = FirebaseDatabase.instance.ref( + 'tests/data-snapshot-${DateTime.now().microsecondsSinceEpoch}-${testCount++}', + ); // Wipe the database before each test await ref.remove(); diff --git a/tests/integration_test/firebase_database/database_reference_e2e.dart b/tests/integration_test/firebase_database/database_reference_e2e.dart index 0c59b4af6ccb..8db764dc4f7f 100644 --- a/tests/integration_test/firebase_database/database_reference_e2e.dart +++ b/tests/integration_test/firebase_database/database_reference_e2e.dart @@ -128,6 +128,7 @@ void setupDatabaseReferenceTests() { test('does not emit local transaction events when disabled', () async { final ref = database.ref('tests/transaction-apply-locally-false'); + await ref.remove(); await ref.set({'count': 0}); final initialEvent = Completer(); diff --git a/tests/integration_test/firebase_database/query_e2e.dart b/tests/integration_test/firebase_database/query_e2e.dart index 8b2ec17a3da5..d849ee96a973 100644 --- a/tests/integration_test/firebase_database/query_e2e.dart +++ b/tests/integration_test/firebase_database/query_e2e.dart @@ -12,9 +12,12 @@ import 'package:flutter_test/flutter_test.dart'; void setupQueryTests() { group('Query', () { late DatabaseReference ref; + var testCount = 0; setUp(() async { - ref = FirebaseDatabase.instance.ref('tests'); + ref = FirebaseDatabase.instance.ref( + 'tests/query-${DateTime.now().microsecondsSinceEpoch}-${testCount++}', + ); // Wipe the database before each test await ref.remove(); From 4e88909bb7d3f020fb7d53bb3f770fff3cae16e3 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 15:27:14 +0200 Subject: [PATCH 02/10] improve --- .github/workflows/web.yaml | 62 +++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 9da8e698b4bf..47b48e9a8031 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -4,9 +4,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-web cancel-in-progress: true -permissions: - contents: read - on: pull_request: paths-ignore: @@ -92,16 +89,59 @@ jobs: # Web devices are not supported for the `flutter test` command yet. As a # workaround we can use the `flutter drive` command. Tracking issue: # https://github.com/flutter/flutter/issues/66264 + # Chrome debug service can fail with AppConnectionException. Retry once. run: | chromedriver --port=4444 --trace-buffer-size=100000 & - flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --dart-define=CI=true | tee output.log - # We have to check the output for failed tests matching the string "[E]" - output=$( Date: Wed, 8 Jul 2026 15:41:06 +0200 Subject: [PATCH 03/10] clean --- .github/workflows/scripts/database.rules.json | 6 +++ .../document_reference_e2e.dart | 4 +- .../example/integration_test/query_e2e.dart | 50 ++++++++++++------- .../integration_test/websocket_e2e.dart | 5 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/.github/workflows/scripts/database.rules.json b/.github/workflows/scripts/database.rules.json index 2b4424611836..0165895dd13a 100644 --- a/.github/workflows/scripts/database.rules.json +++ b/.github/workflows/scripts/database.rules.json @@ -32,6 +32,12 @@ ".read": true, ".write": true, ".indexOn": [".value"] + }, + "$test": { + ".indexOn": [ + ".value", + "number" + ] } } } diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart index 9063b00f15b6..fd611f7f0ef7 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart @@ -630,7 +630,7 @@ void runDocumentReferenceTests() { ), ); await expectLater( - fooConverter.get(), + fooConverter.get(const GetOptions(source: Source.server)), completion( isA>().having((e) => e.data(), 'data', 42), ), @@ -647,7 +647,7 @@ void runDocumentReferenceTests() { ); await expectLater( - fooConverter.get(), + fooConverter.get(const GetOptions(source: Source.server)), completion( isA>().having((e) => e.data(), 'data', 21), ), diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart index 4ef24e41b467..51dce01ede18 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart @@ -7,11 +7,34 @@ import 'package:firebase_data_connect_example/generated/movies.dart'; import 'package:flutter_test/flutter_test.dart'; Future deleteAllMovies() async { - final value = await MoviesConnector.instance.listMovies().ref().execute(); - final result = value.data; - for (var movie in result.movies) { - await MoviesConnector.instance.deleteMovie(id: movie.id).ref().execute(); + for (var attempt = 0; attempt < 5; attempt++) { + final value = await MoviesConnector.instance + .listMovies() + .ref() + .execute(fetchPolicy: QueryFetchPolicy.serverOnly); + final movies = value.data.movies; + if (movies.isEmpty) { + return; + } + + for (final movie in movies) { + await MoviesConnector.instance.deleteMovie(id: movie.id).ref().execute(); + } } + + final value = await MoviesConnector.instance + .listMovies() + .ref() + .execute(fetchPolicy: QueryFetchPolicy.serverOnly); + expect(value.data.movies, isEmpty); +} + +Future> listMoviesFromServer() async { + final value = await MoviesConnector.instance + .listMovies() + .ref() + .execute(fetchPolicy: QueryFetchPolicy.serverOnly); + return value.data.movies; } void runQueryTests() { @@ -23,11 +46,8 @@ void runQueryTests() { }); testWidgets('can query', (WidgetTester tester) async { - final value = - await MoviesConnector.instance.listMovies().ref().execute(); - - final result = value.data; - expect(result.movies.length, 0); + final movies = await listMoviesFromServer(); + expect(movies, isEmpty); }); testWidgets('can add a movie', (WidgetTester tester) async { @@ -61,10 +81,8 @@ void runQueryTests() { .people[0] .id; - final value = - await MoviesConnector.instance.listMovies().ref().execute(); - final result = value.data; - expect(result.movies.length, 0); + final movies = await listMoviesFromServer(); + expect(movies, isEmpty); ref = MoviesConnector.instance .createMovie( @@ -123,10 +141,8 @@ void runQueryTests() { await ref.execute(); - final value2 = - await MoviesConnector.instance.listMovies().ref().execute(); - final result2 = value2.data; - expect(result2.movies.length, 0); + final movies = await listMoviesFromServer(); + expect(movies, isEmpty); }); }, ); diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart index a105b4783184..792d3e632e3b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart @@ -144,9 +144,8 @@ void runWebSocketTests() { await _waitForStreamEvent(isReady.future, 'listMovies subscription'); // Now perform a query, which should go over WebSocket if connected - final result = - await MoviesConnector.instance.listMovies().ref().execute(); - expect(result.data.movies.length, 0); + final movies = await listMoviesFromServer(); + expect(movies, isEmpty); // Perform a mutation await MoviesConnector.instance From 0520ed8a9d3972acd42d25c6bb6f45f179a138bb Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 16:29:49 +0200 Subject: [PATCH 04/10] fix --- tests/integration_test/firebase_auth/test_utils.dart | 8 +++++++- .../firebase_database/database_reference_e2e.dart | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/integration_test/firebase_auth/test_utils.dart b/tests/integration_test/firebase_auth/test_utils.dart index 2a2b1db23aa4..8e6e7e659b3a 100644 --- a/tests/integration_test/firebase_auth/test_utils.dart +++ b/tests/integration_test/firebase_auth/test_utils.dart @@ -240,7 +240,13 @@ Future ensureSignedIn(String testEmail) async { Future ensureSignedOut() async { if (FirebaseAuth.instance.currentUser != null) { - await FirebaseAuth.instance.signOut(); + try { + await FirebaseAuth.instance.signOut(); + } on FirebaseAuthException catch (e) { + if (e.code != 'user-token-expired') { + rethrow; + } + } } } diff --git a/tests/integration_test/firebase_database/database_reference_e2e.dart b/tests/integration_test/firebase_database/database_reference_e2e.dart index 8db764dc4f7f..4de90287cb8b 100644 --- a/tests/integration_test/firebase_database/database_reference_e2e.dart +++ b/tests/integration_test/firebase_database/database_reference_e2e.dart @@ -11,6 +11,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'firebase_database_e2e_test.dart'; +DatabaseReference _uniqueRef(String name) { + return database.ref( + 'tests/$name-${DateTime.now().microsecondsSinceEpoch}', + ); +} + void setupDatabaseReferenceTests() { group('DatabaseReference', () { group('set()', () { @@ -127,8 +133,7 @@ void setupDatabaseReferenceTests() { }); test('does not emit local transaction events when disabled', () async { - final ref = database.ref('tests/transaction-apply-locally-false'); - await ref.remove(); + final ref = _uniqueRef('transaction-apply-locally-false'); await ref.set({'count': 0}); final initialEvent = Completer(); @@ -209,8 +214,7 @@ void setupDatabaseReferenceTests() { }); test('Server.increment', () async { - final FirebaseDatabase database = FirebaseDatabase.instance; - final DatabaseReference ref = database.ref('tests/server-increment'); + final DatabaseReference ref = _uniqueRef('server-increment'); await ref.set(ServerValue.increment(1.5)); final snap = await ref.get(); From 934f372554902d3c20bb9a4ba9eac04fa38dbe9b Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 16:49:27 +0200 Subject: [PATCH 05/10] fix --- .github/workflows/web.yaml | 62 +++++++++++- .../example/integration_test/query_e2e.dart | 94 +++++++++++++------ .../firebase_auth/test_utils.dart | 44 ++++++--- 3 files changed, 154 insertions(+), 46 deletions(-) diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 47b48e9a8031..6c73ded94f00 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -97,6 +97,13 @@ jobs: import subprocess import sys + def normalize_output(output): + if output is None: + return '' + if isinstance(output, bytes): + return output.decode(errors='replace') + return output + command = [ 'flutter', 'drive', @@ -117,16 +124,17 @@ jobs: timeout=180, ) except subprocess.TimeoutExpired as error: - output = error.stdout or '' + output = normalize_output(error.stdout) print(output, end='') with open('output.log', 'w') as file: file.write(output) print('flutter drive timed out before connecting to Chrome.') sys.exit(124) - print(completed.stdout, end='') + output = normalize_output(completed.stdout) + print(output, end='') with open('output.log', 'w') as file: - file.write(completed.stdout) + file.write(output) sys.exit(completed.returncode) PY exit_code=$? @@ -208,12 +216,58 @@ jobs: run: | chromedriver --port=4444 --trace-buffer-size=100000 & run_tests() { - flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --dart-define=CI=true --dart-define=APP_CHECK_E2E=true | tee output.log + python3 - <<'PY' + import subprocess + import sys + + def normalize_output(output): + if output is None: + return '' + if isinstance(output, bytes): + return output.decode(errors='replace') + return output + + command = [ + 'flutter', + 'drive', + '--target=./integration_test/e2e_test.dart', + '--driver=./test_driver/integration_test.dart', + '-d', + 'chrome', + '--dart-define=CI=true', + '--dart-define=APP_CHECK_E2E=true', + ] + + try: + completed = subprocess.run( + command, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=180, + ) + except subprocess.TimeoutExpired as error: + output = normalize_output(error.stdout) + print(output, end='') + with open('output.log', 'w') as file: + file.write(output) + print('flutter drive timed out before connecting to Chrome.') + sys.exit(124) + + output = normalize_output(completed.stdout) + print(output, end='') + with open('output.log', 'w') as file: + file.write(output) + sys.exit(completed.returncode) + PY + exit_code=$? output=$(>> stream = collection.snapshots(); - final snapshots = >>[]; - final receivedAll = Completer(); + final initialSnapshot = + Completer>>(); + final doc1Set = Completer>>(); + final doc1Deleted = Completer>>(); + final doc2Set = Completer>>(); + final doc2Updated = Completer>>(); StreamSubscription subscription = stream.listen((snapshot) { - snapshots.add(snapshot); - if (snapshots.length >= 5 && !receivedAll.isCompleted) { - receivedAll.complete(); + final doc1 = + snapshot.docs.where((doc) => doc.id == 'doc1').firstOrNull; + final doc2 = + snapshot.docs.where((doc) => doc.id == 'doc2').firstOrNull; + + if (!initialSnapshot.isCompleted && + snapshot.docs.length == 1 && + snapshot.docs.single.data()['foo'] == 'bar') { + initialSnapshot.complete(snapshot); + } else if (!doc1Set.isCompleted && + snapshot.docs.length == 2 && + doc1?.data()['bar'] == 'baz') { + doc1Set.complete(snapshot); + } else if (doc1Set.isCompleted && + !doc1Deleted.isCompleted && + doc1 == null) { + doc1Deleted.complete(snapshot); + } else if (doc1Deleted.isCompleted && + !doc2Set.isCompleted && + doc2?.data()['foo'] == 'bar') { + doc2Set.complete(snapshot); + } else if (doc2Set.isCompleted && + !doc2Updated.isCompleted && + doc2?.data()['foo'] == 'baz') { + doc2Updated.complete(snapshot); } }); - // Wait for initial snapshot before making changes - await Future.delayed(const Duration(milliseconds: 500)); - await collection.doc('doc1').set({'bar': 'baz'}); - await collection.doc('doc1').delete(); - await collection.doc('doc2').set({'foo': 'bar'}); - await collection.doc('doc2').update({'foo': 'baz'}); - - // Wait for all 5 snapshots with a timeout instead of hanging forever - await receivedAll.future.timeout(const Duration(seconds: 30)); + final initial = await initialSnapshot.future.timeout( + const Duration(seconds: 30), + ); + expect(initial.docs, hasLength(1)); - expect(snapshots[0].docs.length, equals(1)); - expect(snapshots[0].docs[0].data()['foo'], equals('bar')); + await collection.doc('doc1').set({'bar': 'baz'}); + final withDoc1 = await doc1Set.future.timeout( + const Duration(seconds: 30), + ); + expect(withDoc1.docs, hasLength(2)); + expect( + withDoc1.docs.firstWhere((doc) => doc.id == 'doc1').data()['bar'], + equals('baz'), + ); - expect(snapshots[1].docs.length, equals(2)); - final doc1 = snapshots[1].docs.firstWhere((doc) => doc.id == 'doc1'); - expect(doc1.data()['bar'], equals('baz')); + await collection.doc('doc1').delete(); + final withoutDoc1 = await doc1Deleted.future.timeout( + const Duration(seconds: 30), + ); + expect(withoutDoc1.docs.where((doc) => doc.id == 'doc1'), isEmpty); - expect(snapshots[2].docs.length, equals(1)); + await collection.doc('doc2').set({'foo': 'bar'}); + final withDoc2 = await doc2Set.future.timeout( + const Duration(seconds: 30), + ); expect( - snapshots[2].docs.where((doc) => doc.id == 'doc1').isEmpty, - isTrue, + withDoc2.docs.firstWhere((doc) => doc.id == 'doc2').data()['foo'], + equals('bar'), ); - expect(snapshots[3].docs.length, equals(2)); - final doc2set = snapshots[3].docs.firstWhere((doc) => doc.id == 'doc2'); - expect(doc2set.data()['foo'], equals('bar')); - - expect(snapshots[4].docs.length, equals(2)); - final doc2update = - snapshots[4].docs.firstWhere((doc) => doc.id == 'doc2'); - expect(doc2update.data()['foo'], equals('baz')); + await collection.doc('doc2').update({'foo': 'baz'}); + final updatedDoc2 = await doc2Updated.future.timeout( + const Duration(seconds: 30), + ); + expect( + updatedDoc2.docs.firstWhere((doc) => doc.id == 'doc2').data()['foo'], + equals('baz'), + ); await subscription.cancel(); }); diff --git a/tests/integration_test/firebase_auth/test_utils.dart b/tests/integration_test/firebase_auth/test_utils.dart index 8e6e7e659b3a..bf7d0773e15f 100644 --- a/tests/integration_test/firebase_auth/test_utils.dart +++ b/tests/integration_test/firebase_auth/test_utils.dart @@ -217,24 +217,36 @@ String emulatorCreateCustomToken( } Future ensureSignedIn(String testEmail) async { - if (FirebaseAuth.instance.currentUser == null) { + final currentUser = FirebaseAuth.instance.currentUser; + if (currentUser?.email == testEmail) { try { + await currentUser!.getIdToken(); + return; + } on FirebaseAuthException catch (e) { + if (e.code != 'user-token-expired' && e.code != 'user-not-found') { + rethrow; + } + } + } + + try { + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: testEmail, + password: testPassword, + ); + } on FirebaseAuthException catch (e) { + // The Windows error codes are not consistent + if (e.code == 'user-not-found') { await FirebaseAuth.instance.createUserWithEmailAndPassword( email: testEmail, password: testPassword, ); - } on FirebaseAuthException catch (e) { - // The Windows error codes are not consistent - if (e.code == 'email-already-in-use') { - await FirebaseAuth.instance.signInWithEmailAndPassword( - email: testEmail, - password: testPassword, - ); - } - } catch (e) { - // ignore: avoid_print - print('ensureSignedIn Error $e'); + } else if (e.code != 'email-already-in-use') { + rethrow; } + } catch (e) { + // ignore: avoid_print + print('ensureSignedIn Error $e'); } } @@ -246,6 +258,14 @@ Future ensureSignedOut() async { if (e.code != 'user-token-expired') { rethrow; } + + // Web can keep a deleted emulator user in memory. Replacing it with a + // temporary user gives signOut a valid session to clear local state. + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: generateRandomEmail(prefix: 'signout-'), + password: testPassword, + ); + await FirebaseAuth.instance.signOut(); } } } From aae8ff920f86c0966e7aa64c1f24feac88023b49 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 17:08:26 +0200 Subject: [PATCH 06/10] again --- .github/workflows/web.yaml | 82 +++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 6c73ded94f00..9856fb757732 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -89,10 +89,25 @@ jobs: # Web devices are not supported for the `flutter test` command yet. As a # workaround we can use the `flutter drive` command. Tracking issue: # https://github.com/flutter/flutter/issues/66264 - # Chrome debug service can fail with AppConnectionException. Retry once. + # Chrome debug service can fail with AppConnectionException. Retry with + # a clean browser/driver process set so stale children cannot poison + # the next attempt. run: | - chromedriver --port=4444 --trace-buffer-size=100000 & + cleanup_web_processes() { + pkill -f "Google Chrome" || true + pkill -f chrome_crashpad || true + pkill -x chromedriver || true + pkill -x dartvm || true + pkill -x dartaotruntime || true + } + run_tests() { + rm -f output.log + cleanup_web_processes + chromedriver --port=4444 --trace-buffer-size=100000 & + chromedriver_pid=$! + sleep 2 + python3 - <<'PY' import subprocess import sys @@ -138,18 +153,30 @@ jobs: sys.exit(completed.returncode) PY exit_code=$? + kill "$chromedriver_pid" 2>/dev/null || true + wait "$chromedriver_pid" 2>/dev/null || true + cleanup_web_processes + output=$(/dev/null || true + wait "$chromedriver_pid" 2>/dev/null || true + cleanup_web_processes + output=$( Date: Wed, 8 Jul 2026 17:37:12 +0200 Subject: [PATCH 07/10] fix --- .github/workflows/web.yaml | 96 ++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 9856fb757732..da2609e933c2 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -396,17 +396,95 @@ jobs: # Web devices are not supported for the `flutter test` command yet. As a # workaround we can use the `flutter drive` command. Tracking issue: # https://github.com/flutter/flutter/issues/66264 + # WASM web runs can hang after building but before the test harness + # connects. Retry from a clean browser/driver process set. run: | - chromedriver --port=4444 --trace-buffer-size=100000 & mv ./web/wasm_index.html ./web/index.html - flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --wasm --dart-define=CI=true | tee output.log - # We have to check the output for failed tests matching the string "[E]" - output=$(/dev/null || true + wait "$chromedriver_pid" 2>/dev/null || true + cleanup_web_processes + + output=$( Date: Wed, 8 Jul 2026 17:57:37 +0200 Subject: [PATCH 08/10] fix? --- .../dataconnect/connector/mutations.gql | 6 ++ .../example/integration_test/query_e2e.dart | 25 ++----- .../lib/generated/delete_all_movie_data.dart | 71 +++++++++++++++++++ .../example/lib/generated/movies.dart | 8 +++ 4 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_all_movie_data.dart diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql index 6263b482f9b9..53e9c8edf247 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql @@ -62,6 +62,12 @@ mutation deleteMovie($id: UUID!) @auth(level: PUBLIC) { movie_delete(id: $id) } +mutation deleteAllMovieData @auth(level: PUBLIC) { + directedBy_deleteMany(all: true) + movie_deleteMany(all: true) + person_deleteMany(all: true) +} + mutation thing($title: Any! = "ABC") @auth(level: PUBLIC) { abc: thing_insert(data: { id: "a231d1ff-1825-447d-8b12-de092fb3a0f1" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart index 51dce01ede18..3843e29e7c80 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart @@ -7,26 +7,15 @@ import 'package:firebase_data_connect_example/generated/movies.dart'; import 'package:flutter_test/flutter_test.dart'; Future deleteAllMovies() async { - for (var attempt = 0; attempt < 5; attempt++) { - final value = await MoviesConnector.instance - .listMovies() - .ref() - .execute(fetchPolicy: QueryFetchPolicy.serverOnly); - final movies = value.data.movies; - if (movies.isEmpty) { - return; - } - - for (final movie in movies) { - await MoviesConnector.instance.deleteMovie(id: movie.id).ref().execute(); - } + for (var attempt = 0; attempt < 3; attempt++) { + await MoviesConnector.instance.deleteAllMovieData().ref().execute(); + + final movies = await listMoviesFromServer(); + if (movies.isEmpty) return; } - final value = await MoviesConnector.instance - .listMovies() - .ref() - .execute(fetchPolicy: QueryFetchPolicy.serverOnly); - expect(value.data.movies, isEmpty); + final movies = await listMoviesFromServer(); + expect(movies, isEmpty); } Future> listMoviesFromServer() async { diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_all_movie_data.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_all_movie_data.dart new file mode 100644 index 000000000000..c71d5c27a826 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_all_movie_data.dart @@ -0,0 +1,71 @@ +part of 'movies.dart'; + +class DeleteAllMovieDataVariablesBuilder { + final FirebaseDataConnect _dataConnect; + DeleteAllMovieDataVariablesBuilder( + this._dataConnect, + ); + Deserializer dataDeserializer = + (dynamic json) => DeleteAllMovieDataData.fromJson(jsonDecode(json)); + + Future> execute() { + return ref().execute(); + } + + MutationRef ref() { + return _dataConnect.mutation( + "deleteAllMovieData", dataDeserializer, emptySerializer, null); + } +} + +@immutable +class DeleteAllMovieDataData { + final int directedByDeleteMany; + final int movieDeleteMany; + final int personDeleteMany; + DeleteAllMovieDataData.fromJson(dynamic json) + : directedByDeleteMany = nativeFromJson( + json['directedBy_deleteMany'], + ), + movieDeleteMany = nativeFromJson( + json['movie_deleteMany'], + ), + personDeleteMany = nativeFromJson( + json['person_deleteMany'], + ); + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + + final DeleteAllMovieDataData otherTyped = other as DeleteAllMovieDataData; + return directedByDeleteMany == otherTyped.directedByDeleteMany && + movieDeleteMany == otherTyped.movieDeleteMany && + personDeleteMany == otherTyped.personDeleteMany; + } + + @override + int get hashCode => Object.hashAll([ + directedByDeleteMany.hashCode, + movieDeleteMany.hashCode, + personDeleteMany.hashCode, + ]); + + Map toJson() { + Map json = {}; + json['directedBy_deleteMany'] = nativeToJson(directedByDeleteMany); + json['movie_deleteMany'] = nativeToJson(movieDeleteMany); + json['person_deleteMany'] = nativeToJson(personDeleteMany); + return json; + } + + const DeleteAllMovieDataData({ + required this.directedByDeleteMany, + required this.movieDeleteMany, + required this.personDeleteMany, + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart index 7ad7e907c59a..a6ca68e5e0f3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart @@ -18,6 +18,8 @@ part 'create_movie.dart'; part 'delete_movie.dart'; +part 'delete_all_movie_data.dart'; + part 'thing.dart'; part 'seed_data.dart'; @@ -95,6 +97,12 @@ class MoviesConnector { ); } + DeleteAllMovieDataVariablesBuilder deleteAllMovieData() { + return DeleteAllMovieDataVariablesBuilder( + dataConnect, + ); + } + ThingVariablesBuilder thing() { return ThingVariablesBuilder( dataConnect, From 7b5038e4a042c7065043a1fdf4f673c3a1ca5506 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 18:08:55 +0200 Subject: [PATCH 09/10] fix --- .github/workflows/all_plugins.yaml | 12 ++- .../example/integration_test/listen_e2e.dart | 76 ++++++++++--------- .../integration_test/websocket_e2e.dart | 70 +++++++++-------- 3 files changed, 89 insertions(+), 69 deletions(-) diff --git a/.github/workflows/all_plugins.yaml b/.github/workflows/all_plugins.yaml index ca840d494f10..aabd2f955046 100644 --- a/.github/workflows/all_plugins.yaml +++ b/.github/workflows/all_plugins.yaml @@ -91,7 +91,17 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with: fetch-depth: 0 - - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 + - id: setup-flutter-format + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 + continue-on-error: true + with: + channel: 'stable' + cache: true + cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" + pub-cache-key: "flutter-pub-:os:-:channel:-:version:-:arch:-:hash:" + - name: Retry Flutter setup + if: steps.setup-flutter-format.outcome == 'failure' + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 with: channel: 'stable' cache: true diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart index 7e84491890ae..1bdd62287498 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart @@ -10,6 +10,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'query_e2e.dart'; +const _listenTimeout = Duration(seconds: 30); + void runListenTests() { group( '$FirebaseDataConnect.instance listen', @@ -25,9 +27,8 @@ void runListenTests() { expect(initialValue.data.movies.length, 0, reason: 'Initial movie list should be empty'); - final Completer isReady = Completer(); - final Completer hasBeenListened = Completer(); - int count = 0; + final initialMovies = Completer>(); + final updatedMovies = Completer>(); final listener = MoviesConnector.instance .listMovies() @@ -36,47 +37,48 @@ void runListenTests() { .listen((value) { final movies = value.data.movies; - if (count == 0) { - expect(movies.length, 0, - reason: 'First emission should contain an empty list'); - isReady.complete(); - } else { - expect(movies.length, 1, - reason: 'Second emission should contain one movie'); - expect(movies[0].title, 'The Matrix', - reason: 'The movie should be The Matrix'); - hasBeenListened.complete(true); + if (!initialMovies.isCompleted && movies.isEmpty) { + initialMovies.complete(movies); + } else if (!updatedMovies.isCompleted && + movies.length == 1 && + movies.single.title == 'The Matrix') { + updatedMovies.complete(movies); } - count++; }); - // Wait for the listener to be ready - await isReady.future; - - // Create the movie - await MoviesConnector.instance - .createMovie( - genre: 'Action', - title: 'The Matrix', - releaseYear: 1999, - ) - .rating(4.5) - .ref() - .execute(); + try { + // Wait for the listener to be ready + final initial = await initialMovies.future.timeout(_listenTimeout); + expect(initial, isEmpty, + reason: 'First emission should contain an empty list'); - await MoviesConnector.instance - .listMovies() - .ref() - .execute(fetchPolicy: QueryFetchPolicy.serverOnly); + // Create the movie + await MoviesConnector.instance + .createMovie( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + ) + .rating(4.5) + .ref() + .execute(); - // Wait for the listener to receive the movie update - final bool hasListenerReceived = await hasBeenListened.future; + await MoviesConnector.instance + .listMovies() + .ref() + .execute(fetchPolicy: QueryFetchPolicy.serverOnly); - // Cancel the listener and wait for it to finish - await listener.cancel(); + // Wait for the listener to receive the movie update + final movies = await updatedMovies.future.timeout(_listenTimeout); - expect(hasListenerReceived, isTrue, - reason: 'The stream should have emitted new data'); + expect(movies, hasLength(1), + reason: 'Second emission should contain one movie'); + expect(movies.single.title, 'The Matrix', + reason: 'The movie should be The Matrix'); + } finally { + // Cancel the listener and wait for it to finish + await listener.cancel(); + } }); }, ); diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart index 792d3e632e3b..53bf21f06842 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/websocket_e2e.dart @@ -187,31 +187,35 @@ void runWebSocketTests() { count++; }); - await _waitForStreamEvent(isReady.future, 'listMovies subscription'); - - // Cancel the subscription - await sub.cancel(); - - // Create a movie - await MoviesConnector.instance - .createMovie( - genre: 'Action', - title: 'Avatar', - releaseYear: 2009, - ) - .rating(4.7) - .ref() - .execute(); - - // Wait a bit to ensure no event is received - bool received = true; try { - await receivedUpdate.future.timeout(const Duration(seconds: 2)); - } on TimeoutException { - received = false; + await _waitForStreamEvent(isReady.future, 'listMovies subscription'); + + // Cancel the subscription + await sub.cancel(); + + // Create a movie + await MoviesConnector.instance + .createMovie( + genre: 'Action', + title: 'Avatar', + releaseYear: 2009, + ) + .rating(4.7) + .ref() + .execute(); + + // Wait a bit to ensure no event is received + bool received = true; + try { + await receivedUpdate.future.timeout(const Duration(seconds: 2)); + } on TimeoutException { + received = false; + } + expect(received, isFalse, + reason: 'Should not receive events after cancel'); + } finally { + await sub.cancel(); } - expect(received, isFalse, - reason: 'Should not receive events after cancel'); }); testWidgets( @@ -231,18 +235,22 @@ void runWebSocketTests() { count++; }); - await _waitForStreamEvent(isReady.future, 'listMovies subscription'); + try { + await _waitForStreamEvent(isReady.future, 'listMovies subscription'); - final dataConnect = MoviesConnector.instance.dataConnect; - final transport = (dataConnect as dynamic).transport; - final ws = (transport as dynamic).websocket; + final dataConnect = MoviesConnector.instance.dataConnect; + final transport = (dataConnect as dynamic).transport; + final ws = (transport as dynamic).websocket; - expect(ws.isConnected, isTrue); + expect(ws.isConnected, isTrue); - // Cancel the subscription - await sub.cancel(); + // Cancel the subscription + await sub.cancel(); - expect(ws.isConnected, isFalse); + expect(ws.isConnected, isFalse); + } finally { + await sub.cancel(); + } }); }, ); From 55926f8603c55dc5042ddbf873c7b6906780c697 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 8 Jul 2026 18:42:09 +0200 Subject: [PATCH 10/10] fix --- .github/workflows/e2e_tests_fdc.yaml | 15 +-- .github/workflows/e2e_tests_pipeline.yaml | 13 +- .../scripts/flutter-drive-web-retry.sh | 111 ++++++++++++++++++ 3 files changed, 123 insertions(+), 16 deletions(-) create mode 100755 .github/workflows/scripts/flutter-drive-web-retry.sh diff --git a/.github/workflows/e2e_tests_fdc.yaml b/.github/workflows/e2e_tests_fdc.yaml index 927b412e68d0..7546d63ad647 100644 --- a/.github/workflows/e2e_tests_fdc.yaml +++ b/.github/workflows/e2e_tests_fdc.yaml @@ -324,16 +324,13 @@ jobs: # Web devices are not supported for the `flutter test` command yet. As a # workaround we can use the `flutter drive` command. Tracking issue: # https://github.com/flutter/flutter/issues/66264 + # Chrome debug service can fail with AppConnectionException before tests + # connect. Retry only those infrastructure startup failures. run: | - chromedriver --port=4444 --trace-buffer-size=100000 & - flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --dart-define=CI=true | tee output.log - # We have to check the output for failed tests matching the string "[E]" - output=$(/dev/null || true + wait "$chromedriver_pid" 2>/dev/null || true + cleanup_web_processes + + output=$(