import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:miler/views/Dashboard/profile/informations/faq.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/miler_type.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; /// The pages you can navigate *into* from Account. /// /// ── Why these needed a test ── /// /// Eight pushed pages had six different app bars between them: four solid brand /// red with white text, two white with black text, one grey, one white with a /// red title. The back arrow came in four different icons at four sizes in three /// colours. Two shared helpers already existed and, between them, were used by /// three pages. /// /// And the FAQ page **overflowed at any text scale at or above 1.5** — three /// separate places, on every screen width — which is a black-and-yellow band /// across a help page for the riders most likely to need help reading it. /// /// Both classes of bug are invisible in a screenshot taken at default settings /// on the developer's own phone, so both are measured here instead. void main() { Future pump( WidgetTester tester, Widget page, { double width = 390, double textScale = 1.0, }) async { tester.view.physicalSize = Size(width * 3, 800 * 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( builder: (context, child) => MediaQuery( data: MediaQuery.of( context, ).copyWith(textScaler: TextScaler.linear(textScale)), child: child!, ), home: page, ), ), ); await tester.pumpAndSettle(); } group('the shared pushed-page bar', () { // ── The wash gave way to the brand, everywhere at once ── // // This bar wore a frosted 7% brand wash flattened against white while Home // and the four tabs are solid brand. So pushing Edit Profile, Saved // Addresses, Alert Sound or Notifications changed the colour of the top of // the phone AND the colour of the status-bar glyphs — which reads as // leaving the app rather than opening a page inside it. // // One ground now, and everything on it inverts. The contract this test // still holds is the one that matters: whatever the bar's surface is, the // type on it has to clear contrast against it, and every bar in the app has // to agree on the answer. testWidgets('is solid brand with a white title', (tester) async { await pump( tester, Scaffold(appBar: milerPageBar('Saved Addresses', onBack: () {})), ); final bar = tester.widget(find.byType(AppBar)); expect(bar.backgroundColor, ColorConstants.primary); expect( bar.flexibleSpace, isNull, reason: 'a solid ground needs no surface layered over it', ); expect( bar.systemOverlayStyle?.statusBarIconBrightness, Brightness.light, reason: 'dark glyphs on maroon is the one contrast failure a rider ' 'cannot work around by tilting the phone', ); final title = tester.widget(find.text('Saved Addresses')); expect(title.style!.color, Colors.white); expect( title.style!.fontSize, MilerType.pageTitleFixed.fontSize, reason: 'one page-title size across tabs and pushed screens', ); // The back control inverts with it, or it disappears into the ground. final back = tester.widget(find.byIcon(LucideIcons.arrowLeft)); expect(back.color, Colors.white); }); testWidgets('carries a back affordance when given one', (tester) async { var popped = false; await pump( tester, Scaffold( appBar: milerPageBar('Notifications', onBack: () => popped = true), ), ); // ── An arrow, because these pages are pushed ── // // The cross promised dismissal on nine screens you actually go *back* // from, and it was one of three back glyphs in the app. `close_rounded` // survives behind `isModal: true`, for a sheet that genuinely dismisses. expect(find.byIcon(LucideIcons.arrowLeft), findsOneWidget); expect(find.byIcon(LucideIcons.x), findsNothing); await tester.tap(find.byIcon(LucideIcons.arrowLeft)); await tester.pumpAndSettle(); expect(popped, isTrue); }); testWidgets('and none when it is not', (tester) async { await pump(tester, Scaffold(appBar: milerPageBar('Terms'))); expect(find.byIcon(LucideIcons.arrowLeft), findsNothing); expect(find.byIcon(LucideIcons.x), findsNothing); }); testWidgets('a modal presentation still gets a cross', (tester) async { await pump( tester, Scaffold( appBar: milerPageBar('Skip stop', onBack: () {}, isModal: true), ), ); expect(find.byIcon(LucideIcons.x), findsOneWidget); expect(find.byIcon(LucideIcons.arrowLeft), findsNothing); }); testWidgets('survives a long title and a large font', (tester) async { await pump( tester, Scaffold( appBar: milerPageBar( 'A Very Long Page Title That Will Not Fit', onBack: () {}, ), ), width: 320, textScale: 2.0, ); expect(tester.takeException(), isNull); }); // ── One line, not two ── // // The bar used to carry a caption under the title. It was the smallest type // on the screen in the one place a rider reads exactly once, and every // caption duplicated something the page said louder further down — two // sources for one number, which is how they drift apart. testWidgets('carries the page name and nothing under it', (tester) async { await pump(tester, Scaffold(appBar: milerPageBar('Notifications'))); final texts = find.byType(Text); expect(tester.widgetList(texts), hasLength(1)); expect(find.text('Notifications'), findsOneWidget); }); // ── The × is stacked ABOVE the heading, on the same margin ── // // It used to share the row: `[×] Edit Profile`. That pushed every title on // every pushed page 52pt in from the margin, out of line with the content // beneath it, and a control sitting at a title's left at the same optical // weight reads as part of the title rather than as something you press. testWidgets('stacks the close above the title', (tester) async { await pump( tester, Scaffold(appBar: milerPageBar('Edit Profile', onBack: () {})), ); final close = tester.getRect(find.byIcon(LucideIcons.arrowLeft)); final title = tester.getRect(find.text('Edit Profile')); expect( close.bottom, lessThanOrEqualTo(title.top), reason: 'the back control must sit on its own row above the heading', ); expect( close.left, closeTo(title.left, 0.5), reason: 'they share the page margin — an IconButton could not do this, ' 'which is why the close is hand-rolled. See _TopBarClose.', ); }); testWidgets('gives the back control a full 48pt target', (tester) async { await pump( tester, Scaffold(appBar: milerPageBar('Edit Profile', onBack: () {})), ); final box = tester.getSize( find .ancestor( of: find.byIcon(LucideIcons.arrowLeft), matching: find.byType(SizedBox), ) .first, ); // 48, not the 44pt accessibility floor: this is the control a rider hits // one-handed, and the glyph inside stays 24 so only the target grew. expect(box.width, 48); expect(box.height, 48); }); }); group('FAQ lays out at every font scale', () { // It did not. `Container(height: 160.h)` around the hero overflowed by 89px // at 1.5x; the category grid's `childAspectRatio` derived cell height from // the column *width*, so all four tiles overflowed by ~10px; and the SOS // row's two unflexed children ran 30px past the card. for (final width in [320.0, 360.0, 390.0, 430.0]) { for (final scale in [1.0, 1.3, 1.5, 2.0]) { testWidgets('at ${width.toInt()}pt / ${scale}x', (tester) async { await pump(tester, const FaqPage(), width: width, textScale: scale); expect( tester.takeException(), isNull, reason: 'FAQ overflowed at ${width.toInt()}pt / ${scale}x', ); // Opening the accordions is where the page grows the most. final tiles = find.byType(ExpansionTile); for (var i = 0; i < tiles.evaluate().length; i++) { await tester.tap(tiles.at(i), warnIfMissed: false); await tester.pumpAndSettle(); expect( tester.takeException(), isNull, reason: 'FAQ overflowed opening accordion $i at ' '${width.toInt()}pt / ${scale}x', ); } }); } } }); // Terms & Conditions is deliberately not laid out here: it hosts a // `WebView`, and `WebViewPlatform.instance` has no implementation in a headless // unit test, so the page cannot be built at all. Its app bar is the shared one // — that much is covered by the group above — but the body needs a device or // an integration test. }