From 86eeebec9de6e0df3e69854675b3bdeae4b4aa6a Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Thu, 27 Mar 2025 12:11:20 -0700 Subject: [PATCH 1/4] Use a more deterministic way of waiting for ad widgets to be ready. These unit tests were failing when a skwasm change caused a subtle timing difference. See https://github.com/flutter/flutter/issues/165347 --- .../experimental_ad_unit_widget_test.dart | 40 ++++++++++++++----- .../lib/src/adsense/ad_unit_widget.dart | 6 ++- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart b/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart index 4f7cb10562e..9733a4b555b 100644 --- a/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart +++ b/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart @@ -19,6 +19,17 @@ import 'js_interop_mocks/adsense_test_js_interop.dart'; const String testClient = 'test_client'; const String testSlot = 'test_slot'; +class CallbackTracker { + void Function() createCallback() { + final int callbackIndex = _callbackStates.length; + _callbackStates.add(false); + return () => _callbackStates[callbackIndex] = true; + } + + bool get allCalled => _callbackStates.every((bool state) => state); + final List _callbackStates = []; +} + void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -47,15 +58,17 @@ void main() async { await adSense.initialize(testClient); + final CallbackTracker tracker = CallbackTracker(); final Widget adUnitWidget = AdUnitWidget( configuration: AdUnitConfiguration.displayAdUnit( adSlot: testSlot, adFormat: AdFormat.AUTO, // Important! ), adClient: adSense.adClient, + onInjected: tracker.createCallback(), ); - await pumpAdWidget(adUnitWidget, tester); + await pumpAdWidget(adUnitWidget, tester, tracker); // Then // Widget level @@ -81,11 +94,13 @@ void main() async { await adSense.initialize(testClient); + final CallbackTracker tracker = CallbackTracker(); final Widget adUnitWidget = AdUnitWidget( configuration: AdUnitConfiguration.displayAdUnit( adSlot: testSlot, ), adClient: adSense.adClient, + onInjected: tracker.createCallback(), ); final Widget constrainedAd = Container( @@ -93,7 +108,7 @@ void main() async { child: adUnitWidget, ); - await pumpAdWidget(constrainedAd, tester); + await pumpAdWidget(constrainedAd, tester, tracker); // Then // Widget level @@ -110,14 +125,17 @@ void main() async { mockAdsByGoogle(mockAd(adStatus: AdStatus.UNFILLED)); await adSense.initialize(testClient); + + final CallbackTracker tracker = CallbackTracker(); final Widget adUnitWidget = AdUnitWidget( configuration: AdUnitConfiguration.displayAdUnit( adSlot: testSlot, ), adClient: adSense.adClient, + onInjected: tracker.createCallback(), ); - await pumpAdWidget(adUnitWidget, tester); + await pumpAdWidget(adUnitWidget, tester, tracker); // Then expect(find.byType(HtmlElementView), findsNothing, @@ -142,6 +160,7 @@ void main() async { await adSense.initialize(testClient); + final CallbackTracker tracker = CallbackTracker(); final Widget bunchOfAds = Column( children: [ AdUnitWidget( @@ -150,6 +169,7 @@ void main() async { adFormat: AdFormat.AUTO, ), adClient: adSense.adClient, + onInjected: tracker.createCallback(), ), AdUnitWidget( configuration: AdUnitConfiguration.displayAdUnit( @@ -157,6 +177,7 @@ void main() async { adFormat: AdFormat.AUTO, ), adClient: adSense.adClient, + onInjected: tracker.createCallback(), ), Container( constraints: const BoxConstraints(maxHeight: 100), @@ -165,12 +186,13 @@ void main() async { adSlot: testSlot, ), adClient: adSense.adClient, + onInjected: tracker.createCallback(), ), ), ], ); - await pumpAdWidget(bunchOfAds, tester); + await pumpAdWidget(bunchOfAds, tester, tracker); // Then // Widget level @@ -192,7 +214,7 @@ void main() async { } // Pumps an AdUnit Widget into a given tester, with some parameters -Future pumpAdWidget(Widget adUnit, WidgetTester tester) async { +Future pumpAdWidget(Widget adUnit, WidgetTester tester, CallbackTracker tracker) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -203,10 +225,10 @@ Future pumpAdWidget(Widget adUnit, WidgetTester tester) async { ), ); - // This extra pump is needed for the platform view to actually render in the DOM. - await tester.pump(); - // One more for skwasm. - await tester.pump(); + while (!tracker.allCalled) { + // Pump until all the widgets have had their platform views injected into the dom. + await tester.pump(); + } // This extra pump is needed to simulate the async behavior of the adsense JS mock. await tester.pumpAndSettle(); } diff --git a/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart b/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart index 5e25dda6d76..6c0351786b4 100644 --- a/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart +++ b/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart @@ -33,13 +33,16 @@ class AdUnitWidget extends StatefulWidget { super.key, required AdUnitConfiguration configuration, @visibleForTesting String? adClient, + @visibleForTesting void Function()? onInjected, }) : _adClient = adClient ?? adSense.adClient, + _onInjected = onInjected, _adUnitConfiguration = configuration { assert(_adClient != null, 'Attempted to render an AdUnitWidget before calling adSense.initialize'); } final String? _adClient; + final void Function()? _onInjected; final AdUnitConfiguration _adUnitConfiguration; @@ -58,13 +61,14 @@ class _AdUnitWidgetWebState extends State @override bool get wantKeepAlive => true; - static final web.ResizeObserver _adSenseResizeObserver = web.ResizeObserver( + web.ResizeObserver get _adSenseResizeObserver => web.ResizeObserver( (JSArray entries, web.ResizeObserver observer) { for (final web.ResizeObserverEntry entry in entries.toDart) { final web.Element target = entry.target; if (target.isConnected) { // First time resized since attached to DOM -> attachment callback from Flutter docs by David _onElementAttached(target as web.HTMLElement); + widget._onInjected?.call(); observer.disconnect(); } } From 00a2104eb10383d2eab3a3ad69d64960491b1d1a Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Thu, 27 Mar 2025 13:00:31 -0700 Subject: [PATCH 2/4] Bump version --- packages/google_adsense/CHANGELOG.md | 4 ++++ packages/google_adsense/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/google_adsense/CHANGELOG.md b/packages/google_adsense/CHANGELOG.md index 57f202439cc..6eac44ef1bd 100644 --- a/packages/google_adsense/CHANGELOG.md +++ b/packages/google_adsense/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.2 + +* Added a callback to the widget for testing to make unit tests more deterministic. + ## 0.1.1 * Adds `AdSenseCodeParameters` configuration object for `adSense.initialize`. diff --git a/packages/google_adsense/pubspec.yaml b/packages/google_adsense/pubspec.yaml index 26b865b5f4d..9dbb3897d33 100644 --- a/packages/google_adsense/pubspec.yaml +++ b/packages/google_adsense/pubspec.yaml @@ -2,7 +2,7 @@ name: google_adsense description: A wrapper plugin with convenience APIs allowing easier inserting Google Adsense HTML snippets withing a Flutter UI Web application repository: https://github.com/flutter/packages/tree/main/packages/google_adsense issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_adsense%22 -version: 0.1.1 +version: 0.1.2 environment: sdk: ^3.4.0 From 9b4f5e8976a2c89145c4a8fae13041bd33720f36 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Thu, 27 Mar 2025 13:14:05 -0700 Subject: [PATCH 3/4] Add timeout when waiting for platform view to be injected. --- .../integration_test/experimental_ad_unit_widget_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart b/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart index 9733a4b555b..20409c741a0 100644 --- a/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart +++ b/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart @@ -225,7 +225,11 @@ Future pumpAdWidget(Widget adUnit, WidgetTester tester, CallbackTracker tr ), ); + final Stopwatch timer = Stopwatch()..start(); while (!tracker.allCalled) { + if (timer.elapsedMilliseconds > 1000) { + fail('timeout while waiting for ad widget to be injected'); + } // Pump until all the widgets have had their platform views injected into the dom. await tester.pump(); } From a50fd0014ff67822e498cc0408e5237b295925fe Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Thu, 27 Mar 2025 13:23:22 -0700 Subject: [PATCH 4/4] Formatting. --- .../experimental_ad_unit_widget_test.dart | 3 ++- .../lib/src/adsense/ad_unit_widget.dart | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart b/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart index 20409c741a0..54fa26e0a7f 100644 --- a/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart +++ b/packages/google_adsense/example/integration_test/experimental_ad_unit_widget_test.dart @@ -214,7 +214,8 @@ void main() async { } // Pumps an AdUnit Widget into a given tester, with some parameters -Future pumpAdWidget(Widget adUnit, WidgetTester tester, CallbackTracker tracker) async { +Future pumpAdWidget( + Widget adUnit, WidgetTester tester, CallbackTracker tracker) async { await tester.pumpWidget( MaterialApp( home: Scaffold( diff --git a/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart b/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart index 6c0351786b4..f4a06c73a39 100644 --- a/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart +++ b/packages/google_adsense/lib/src/adsense/ad_unit_widget.dart @@ -33,7 +33,7 @@ class AdUnitWidget extends StatefulWidget { super.key, required AdUnitConfiguration configuration, @visibleForTesting String? adClient, - @visibleForTesting void Function()? onInjected, + @visibleForTesting void Function()? onInjected, }) : _adClient = adClient ?? adSense.adClient, _onInjected = onInjected, _adUnitConfiguration = configuration { @@ -61,18 +61,19 @@ class _AdUnitWidgetWebState extends State @override bool get wantKeepAlive => true; - web.ResizeObserver get _adSenseResizeObserver => web.ResizeObserver( - (JSArray entries, web.ResizeObserver observer) { - for (final web.ResizeObserverEntry entry in entries.toDart) { - final web.Element target = entry.target; - if (target.isConnected) { - // First time resized since attached to DOM -> attachment callback from Flutter docs by David - _onElementAttached(target as web.HTMLElement); - widget._onInjected?.call(); - observer.disconnect(); - } - } - }.toJS); + web.ResizeObserver get _adSenseResizeObserver => + web.ResizeObserver((JSArray entries, + web.ResizeObserver observer) { + for (final web.ResizeObserverEntry entry in entries.toDart) { + final web.Element target = entry.target; + if (target.isConnected) { + // First time resized since attached to DOM -> attachment callback from Flutter docs by David + _onElementAttached(target as web.HTMLElement); + widget._onInjected?.call(); + observer.disconnect(); + } + } + }.toJS); @override Widget build(BuildContext context) {