import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:miler/views/helpers/constants/app_theme.dart'; import 'package:miler/views/introscreens/introscreen.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE FIRST-RUN SLIDES /// /// These used to be miniature mock-ups of Home built out of fixed-width rows — /// the hub rail on slide 1 was a dozen sized children in a single unflexed /// `Row` — and the tests existed because that is exactly the shape which /// overflows on a narrow phone. /// /// The slides are finished posters now: headline, sub-copy, product shot and /// the MILER lockup are all baked into the artwork, and the screen draws no /// text of its own. A `BoxFit.cover` image cannot overflow, so the width sweep /// is no longer about the illustrations — it is about the controls laid over /// them, which are the only Flutter-built objects left on the screen. /// /// What is worth pinning instead is the part a wrong asset path would break /// silently: three specific images, in the order the rider is meant to see /// them, with the CTA changing on the last one. /// ───────────────────────────────────────────────────────────────────────── void main() { const slides = [ 'assets/images/onboard_1.png', 'assets/images/onboard_2.png', 'assets/images/onboard_3.png', ]; Future pump(WidgetTester tester, {required Size screen}) async { tester.view.physicalSize = Size(screen.width * 3, screen.height * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp(theme: AppTheme.light, home: const Introscreen()), ), ); await tester.pumpAndSettle(); } /// The asset the slide currently on screen is drawing. String? shownAsset(WidgetTester tester) { for (final image in tester.widgetList(find.byType(Image))) { final provider = image.image; if (provider is AssetImage && provider.assetName.startsWith('assets/images/onboard_')) { return provider.assetName; } } return null; } for (final size in const [ Size(320, 640), // smallest Android still in the fleet Size(360, 740), Size(390, 844), Size(430, 932), ]) { testWidgets('all three slides lay out at ${size.width.toInt()}pt', ( tester, ) async { await pump(tester, screen: size); expect(tester.takeException(), isNull); expect(shownAsset(tester), slides[0]); expect(find.text('Next'), findsOneWidget); expect(find.text('Skip'), findsOneWidget); // A near-full-width drag, so the page always snaps to the next one. A // velocity fling does not reliably cross the threshold here. final swipe = Offset(-size.width * 0.9, 0); await tester.drag(find.byType(PageView), swipe); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(shownAsset(tester), slides[1]); expect(find.text('Next'), findsOneWidget); await tester.drag(find.byType(PageView), swipe); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(shownAsset(tester), slides[2]); // Last slide swaps the CTA and drops Skip — an invisible-but-present // control is a trap for screen readers, so it is unmounted rather than // faded to `opacity: 0`. expect(find.text('Get started'), findsOneWidget); expect(find.text('Next'), findsNothing); expect(find.text('Skip'), findsNothing); }); } testWidgets('the three posters are in the order they were given', ( tester, ) async { await pump(tester, screen: const Size(390, 844)); // `PageView.builder` only builds the page on screen, so the order has to be // walked rather than read out of the tree in one go. Which is the honest // test anyway: what matters is the sequence the rider is shown, and 1-3-2 // would pass any assertion made against a set. final seen = [shownAsset(tester)]; for (var i = 1; i < slides.length; i++) { await tester.drag(find.byType(PageView), const Offset(-351, 0)); await tester.pumpAndSettle(); seen.add(shownAsset(tester)); } expect(seen, slides); }); testWidgets('the artwork runs edge to edge', (tester) async { await pump(tester, screen: const Size(390, 844)); final image = tester.widget( find .byWidgetPredicate( (w) => w is Image && w.image is AssetImage && (w.image as AssetImage).assetName == slides[0], ) .first, ); // `cover` is what makes a 940×1672 poster fill a phone of any aspect // without letterboxing. `contain` would put bars down the sides, which on // artwork that ends in a red wave reads as a rendering fault. expect(image.fit, BoxFit.cover); }); testWidgets('the screen adds no captions of its own', (tester) async { await pump(tester, screen: const Size(390, 844)); // Every slide's headline and sub-copy are baked into the image. A Flutter // caption under the picture would say the same thing twice, in a second // typeface, at a second size — so the only text on the screen is the two // controls. final texts = tester .widgetList(find.byType(Text)) .map((t) => t.data) .whereType() .toSet(); expect(texts, {'Skip', 'Next'}); }); testWidgets('Skip is offered on the first slide', (tester) async { await pump(tester, screen: const Size(390, 844)); expect(find.text('Skip'), findsOneWidget); }); // The old code-drawn copy pitched a gig marketplace — "pickups near you", // accepted from a feed. This product assigns routes from a hub, and // onboarding that teaches the wrong model is worse than none. // // NOTE: this can only speak for text the *app* draws. The posters carry their // own headlines as pixels, and `onboard_1.png` does print "Powered by AI" — // a claim the previous slides deliberately dropped because nothing in the app // backs it. That is a copy decision about the artwork, not something a widget // test can hold. testWidgets('the app draws no marketplace copy of its own', (tester) async { await pump(tester, screen: const Size(390, 844)); for (final wrong in [ 'Find pickups near you', 'Navigate the smart way', 'Earn more, every shift', 'Powered by AI', ]) { expect(find.text(wrong), findsNothing, reason: '"$wrong" is not this app'); } }); }