1+ import 'dart:async' ;
12import 'dart:io' ;
23import 'dart:math' ;
34import 'dart:typed_data' ;
@@ -15,6 +16,10 @@ import 'package:lantern/main.dart' as app;
1516
1617const _stripeHost = 'checkout.stripe.com' ;
1718const _screenshotPath = String .fromEnvironment ('PAYMENT_SMOKE_SCREENSHOT_PATH' );
19+ const _screenshotRenderTimeout = Duration (seconds: 30 );
20+ const _screenshotPollInterval = Duration (milliseconds: 250 );
21+ const _minimumScreenshotContrast = 64 ;
22+ const _darkPixelLuminance = 192 ;
1823
1924void main () {
2025 IntegrationTestWidgetsFlutterBinding .ensureInitialized ();
@@ -108,12 +113,9 @@ void main() {
108113 expect (observer.documentLength, greaterThan (0 ));
109114 if (Platform .isMacOS) {
110115 final screenshot = observer.screenshot;
111- expect (
112- screenshot,
113- isNotNull,
114- reason: 'The Stripe WebView did not return a screenshot' ,
115- );
116- await _verifyScreenshot (screenshot! );
116+ if (screenshot == null ) {
117+ fail ('The Stripe WebView did not return a screenshot' );
118+ }
117119 if (_screenshotPath.isNotEmpty) {
118120 final file = File (_screenshotPath);
119121 await file.parent.create (recursive: true );
@@ -130,14 +132,63 @@ void main() {
130132 );
131133}
132134
133- Future <void > _verifyScreenshot (Uint8List screenshot) async {
134- expect (screenshot.lengthInBytes, greaterThan (1024 ));
135+ Future <Uint8List > _waitForRenderedScreenshot (
136+ Future <Uint8List ?> Function () captureScreenshot,
137+ ) async {
138+ final deadline = DateTime .now ().add (_screenshotRenderTimeout);
139+ Object ? lastError;
140+ while (DateTime .now ().isBefore (deadline)) {
141+ try {
142+ final screenshot = await captureScreenshot ().timeout (
143+ const Duration (seconds: 5 ),
144+ );
145+ if (screenshot != null && await _hasVisibleContent (screenshot)) {
146+ return screenshot;
147+ }
148+ } catch (error) {
149+ lastError = error;
150+ }
151+ await Future <void >.delayed (_screenshotPollInterval);
152+ }
153+ final detail = lastError == null ? '' : ': $lastError ' ;
154+ throw TimeoutException (
155+ 'Stripe Checkout did not become visually ready$detail ' ,
156+ _screenshotRenderTimeout,
157+ );
158+ }
159+
160+ Future <bool > _hasVisibleContent (Uint8List screenshot) async {
161+ if (screenshot.lengthInBytes <= 1024 ) return false ;
162+
135163 final codec = await ui.instantiateImageCodec (screenshot);
136164 try {
137165 final frame = await codec.getNextFrame ();
138166 try {
139- expect (frame.image.width, greaterThan (100 ));
140- expect (frame.image.height, greaterThan (100 ));
167+ if (frame.image.width <= 100 || frame.image.height <= 100 ) return false ;
168+ final data = await frame.image.toByteData (
169+ format: ui.ImageByteFormat .rawRgba,
170+ );
171+ if (data == null ) return false ;
172+
173+ var darkest = 255 ;
174+ var lightest = 0 ;
175+ var darkPixels = 0 ;
176+ final pixelCount = data.lengthInBytes ~ / 4 ;
177+ final minimumDarkPixels = max (64 , pixelCount ~ / 1000 );
178+ for (var offset = 0 ; offset < data.lengthInBytes; offset += 4 ) {
179+ final red = data.getUint8 (offset);
180+ final green = data.getUint8 (offset + 1 );
181+ final blue = data.getUint8 (offset + 2 );
182+ final luminance = (299 * red + 587 * green + 114 * blue) ~ / 1000 ;
183+ if (luminance < darkest) darkest = luminance;
184+ if (luminance > lightest) lightest = luminance;
185+ if (luminance <= _darkPixelLuminance) darkPixels++ ;
186+ if (lightest - darkest >= _minimumScreenshotContrast &&
187+ darkPixels >= minimumDarkPixels) {
188+ return true ;
189+ }
190+ }
191+ return false ;
141192 } finally {
142193 frame.image.dispose ();
143194 }
@@ -195,12 +246,7 @@ class _StripeCheckoutObserver implements AppWebViewObserver {
195246 if (uri.host != _stripeHost) return ;
196247 if (Platform .isMacOS) {
197248 try {
198- screenshot = await captureScreenshot ().timeout (
199- const Duration (seconds: 10 ),
200- );
201- if (screenshot == null || screenshot! .isEmpty) {
202- checkoutFailure = 'WebView screenshot was empty' ;
203- }
249+ screenshot = await _waitForRenderedScreenshot (captureScreenshot);
204250 } catch (error) {
205251 checkoutFailure = 'Unable to capture WebView screenshot: $error ' ;
206252 }
0 commit comments