Skip to content

Commit

Permalink
Merge pull request flutter#3 from ditman/fix-ins-in-integration-test
Browse files Browse the repository at this point in the history
Update tests to match AdSense behavior, and our code.
  • Loading branch information
sokoloff06 authored Dec 1, 2024
2 parents 44fc9e6 + 4e52e92 commit 49a7a75
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 70 deletions.
177 changes: 127 additions & 50 deletions packages/google_adsense/example/integration_test/ad_widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// Ensure we don't use the singleton `adSense`, but the local copies to this plugin.
import 'package:google_adsense/google_adsense.dart' hide adSense;
import 'package:google_adsense/src/ad_unit_widget.dart';
import 'package:integration_test/integration_test.dart';
import 'package:web/web.dart' as web;

Expand All @@ -23,10 +24,10 @@ const String testScriptUrl =
void main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

late AdSense adsense;
late AdSense adSense;

setUp(() async {
adsense = AdSense();
adSense = AdSense();
});

tearDown(() {
Expand All @@ -38,7 +39,7 @@ void main() async {
final web.HTMLElement target = web.HTMLDivElement();
// Given

adsense.initialize(testClient, jsLoaderTarget: target);
adSense.initialize(testClient, jsLoaderTarget: target);

final web.HTMLScriptElement? injected =
target.lastElementChild as web.HTMLScriptElement?;
Expand All @@ -49,100 +50,176 @@ void main() async {
expect(injected.async, true);
});

testWidgets('Skips initialization if script already present.',
testWidgets('Skips initialization if script is already present.',
(WidgetTester _) async {
final web.HTMLScriptElement script = web.HTMLScriptElement()
..id = 'previously-injected'
..src = testScriptUrl;
final web.HTMLElement target = web.HTMLDivElement()..appendChild(script);

adsense.initialize(testClient, jsLoaderTarget: target);
adSense.initialize(testClient, jsLoaderTarget: target);

expect(target.childElementCount, 1);
expect(target.firstElementChild?.id, 'previously-injected');
});

testWidgets('Skips initialization if adsense object already present.',
testWidgets('Skips initialization if adsense object is already present.',
(WidgetTester _) async {
final web.HTMLElement target = web.HTMLDivElement();

// Write an empty noop object
mockAdsByGoogle(() {});

adsense.initialize(testClient, jsLoaderTarget: target);
adSense.initialize(testClient, jsLoaderTarget: target);

expect(target.firstElementChild, isNull);
});
});

group('adWidget', () {
testWidgets('Filled ad units resize widget height',
testWidgets('Responsive (with adFormat) ad units reflow flutter',
(WidgetTester tester) async {
// When
mockAdsByGoogle(() {
// Locate the target element, and push a red div to it...
final web.Element? adTarget =
web.document.querySelector('div[id^=adUnit] ins');

final web.HTMLElement fakeAd = web.HTMLDivElement()
..style.width = '320px'
..style.height = '137px'
..style.background = 'red';
// The size of the ad that we're going to "inject"
const double expectedHeight = 137;

adTarget!
..appendChild(fakeAd)
..setAttribute('data-ad-status', AdStatus.FILLED);
});
// When
mockAdsByGoogle(
mockAd(
size: const Size(320, expectedHeight),
),
);

adsense.initialize(testClient);
adSense.initialize(testClient);

final Widget adUnitWidget =
adsense.adUnit(AdUnitConfiguration.displayAdUnit(adSlot: testSlot));
final Widget adUnitWidget = adSense.adUnit(
AdUnitConfiguration.displayAdUnit(
adSlot: testSlot,
adFormat: AdFormat.AUTO, // Important!
),
);

await pumpAdWidget(adUnitWidget, tester);

// Then
// Widget level
expect(find.byWidget(adUnitWidget), findsOneWidget);
final Finder adUnit = find.byWidget(adUnitWidget);
expect(adUnit, findsOneWidget);

final Size size = tester.getSize(adUnit);
expect(size.height, expectedHeight);
});

testWidgets(
'Fixed size (without adFormat) ad units respect flutter constraints',
(WidgetTester tester) async {
const double maxHeight = 100;
const BoxConstraints constraints = BoxConstraints(maxHeight: maxHeight);

// When
mockAdsByGoogle(
mockAd(
size: const Size(320, 157),
),
);

adSense.initialize(testClient);

final Widget adUnitWidget = adSense.adUnit(
AdUnitConfiguration.displayAdUnit(
adSlot: testSlot,
),
);

final Widget constrainedAd = Container(
constraints: constraints,
child: adUnitWidget,
);

final Size size = tester.getSize(find.byWidget(adUnitWidget));
await pumpAdWidget(constrainedAd, tester);

expect(size.height, 137);
// Then
// Widget level
final Finder adUnit = find.byWidget(adUnitWidget);
expect(adUnit, findsOneWidget);

final Size size = tester.getSize(adUnit);
expect(size.height, maxHeight);
});

testWidgets('Unfilled ad units collapse widget height',
(WidgetTester tester) async {
// When
mockAdsByGoogle(() {
// Locate the target element, and push a red div to it...
final web.Element? adTarget =
web.document.querySelector('div[id^=adUnit] ins');

// The internal styling of the Ad doesn't matter, if AdSense tells us it is UNFILLED.
final web.HTMLElement fakeAd = web.HTMLDivElement()
..style.width = '320px'
..style.height = '137px'
..style.background = 'red';

adTarget!
..appendChild(fakeAd)
..setAttribute('data-ad-status', AdStatus.UNFILLED);
});

adsense.initialize(testClient);
final Widget adUnitWidget =
adsense.adUnit(AdUnitConfiguration.displayAdUnit(adSlot: testSlot));
mockAdsByGoogle(mockAd(adStatus: AdStatus.UNFILLED));

adSense.initialize(testClient);
final Widget adUnitWidget = adSense.adUnit(
AdUnitConfiguration.displayAdUnit(
adSlot: testSlot,
),
);

await pumpAdWidget(adUnitWidget, tester);

// Then
// Widget level
expect(find.byWidget(adUnitWidget), findsOneWidget);
expect(find.byType(HtmlElementView), findsNothing,
reason: 'Unfilled ads should remove their platform view');

final Size size = tester.getSize(find.byWidget(adUnitWidget));
final Finder adUnit = find.byWidget(adUnitWidget);
expect(adUnit, findsOneWidget);

final Size size = tester.getSize(adUnit);
expect(size.height, 0);
});

testWidgets('Can handle multiple ads', (WidgetTester tester) async {
// When
mockAdsByGoogle(
mockAds(<MockAdConfig>[
(size: const Size(320, 200), adStatus: AdStatus.FILLED),
(size: Size.zero, adStatus: AdStatus.UNFILLED),
(size: const Size(640, 90), adStatus: AdStatus.FILLED),
]),
);

adSense.initialize(testClient);

final Widget bunchOfAds = Column(
children: <Widget>[
adSense.adUnit(AdUnitConfiguration.displayAdUnit(
adSlot: testSlot,
adFormat: AdFormat.AUTO,
)),
adSense.adUnit(AdUnitConfiguration.displayAdUnit(
adSlot: testSlot,
adFormat: AdFormat.AUTO,
)),
Container(
constraints: const BoxConstraints(maxHeight: 100),
child: adSense.adUnit(AdUnitConfiguration.displayAdUnit(
adSlot: testSlot,
)),
),
],
);

await pumpAdWidget(bunchOfAds, tester);

// Then
// Widget level
final Finder platformViews = find.byType(HtmlElementView);
expect(platformViews, findsExactly(2),
reason: 'The platform view of unfilled ads should be removed.');

final Finder adUnits = find.byType(AdUnitWidget);
expect(adUnits, findsExactly(3));

expect(tester.getSize(adUnits.at(0)).height, 200,
reason: 'Responsive ad widget should resize to match its `ins`');
expect(tester.getSize(adUnits.at(1)).height, 0,
reason: 'Unfulfilled ad should be 0x0');
expect(tester.getSize(adUnits.at(2)).height, 100,
reason: 'The constrained ad should use the height of container');
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ library;

import 'dart:async';
import 'dart:js_interop';
import 'dart:ui';

import 'package:google_adsense/google_adsense.dart';
import 'package:web/web.dart' as web;

typedef VoidFn = void Function();

// window.adsbygoogle uses "duck typing", so let us set anything to it.
@JS('adsbygoogle')
Expand All @@ -15,7 +21,7 @@ external set _adsbygoogle(JSAny? value);
/// Mocks `adsbygoogle` [push] function.
///
/// `push` will run in the next tick (`Timer.run`) to ensure async behavior.
void mockAdsByGoogle(void Function() push) {
void mockAdsByGoogle(VoidFn push) {
_adsbygoogle = <String, Object>{
'push': () {
Timer.run(push);
Expand All @@ -27,3 +33,58 @@ void mockAdsByGoogle(void Function() push) {
void clearAdsByGoogleMock() {
_adsbygoogle = null;
}

typedef MockAdConfig = ({Size size, String adStatus});

/// Returns a function that generates a "push" function for [mockAdsByGoogle].
VoidFn mockAd({
Size size = Size.zero,
String adStatus = AdStatus.FILLED,
}) {
return mockAds(
<MockAdConfig>[(size: size, adStatus: adStatus)],
);
}

/// Returns a function that handles a bunch of ad units at once. Can be used with [mockAdsByGoogle].
VoidFn mockAds(List<MockAdConfig> adConfigs) {
return () {
final List<web.HTMLElement> foundTargets =
web.document.querySelectorAll('div[id^=adUnit] ins').toList;

for (int i = 0; i < foundTargets.length; i++) {
final web.HTMLElement adTarget = foundTargets[i];
if (adTarget.children.length > 0) {
continue;
}

final (:Size size, :String adStatus) = adConfigs[i];

final web.HTMLElement fakeAd = web.HTMLDivElement()
..style.width = '${size.width}px'
..style.height = '${size.height}px'
..style.background = '#fabada';

// AdSense seems to be setting the width/height on the `ins` of the injected ad too.
adTarget
..style.width = '${size.width}px'
..style.height = '${size.height}px'
..style.display = 'block'
..appendChild(fakeAd)
..setAttribute('data-ad-status', adStatus);
}
};
}

extension on web.NodeList {
List<web.HTMLElement> get toList {
final List<web.HTMLElement> result = <web.HTMLElement>[];
for (int i = 0; i < length; i++) {
final web.Node? node = item(i);
if (node != null && node.isA<web.HTMLElement>()) {
result.add(node as web.HTMLElement);
}
}
return result;
}
}
18 changes: 4 additions & 14 deletions packages/google_adsense/example/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">

<meta charset="UTF-8">
Expand All @@ -37,6 +24,9 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
<script>
/* Inline the contents of flutter_bootstrap.js */
{{flutter_bootstrap_js}}
</script>
</body>
</html>
6 changes: 1 addition & 5 deletions packages/google_adsense/lib/src/ad_unit_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,7 @@ class _AdUnitWidgetWebState extends State<AdUnitWidget>
target.offsetHeight.toDouble(),
));
} else {
// Prevent scrolling issues over empty ad slot
target
..style.pointerEvents = 'none'
..style.height = '0px'
..style.width = '0px';
// This removes the platform view.
_updateWidgetSize(Size.zero);
}
}
Expand Down

0 comments on commit 49a7a75

Please sign in to comment.