import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/core/theme/theme_extensions.dart'; import 'package:motorcycle_dash/features/social/clubs/create_club_screen.dart'; import 'package:motorcycle_dash/services/auth/auth_providers.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/clubs/club_models.dart'; import 'package:motorcycle_dash/services/clubs/club_providers.dart'; import 'package:motorcycle_dash/services/clubs/club_service.dart'; import 'package:motorcycle_dash/services/clubs/club_slug_validator.dart'; class FakeCreateClubService extends ClubService { final Set takenSlugs; Club? lastCreatedClub; bool shouldFailSlugTaken; FakeCreateClubService({ this.takenSlugs = const {}, this.shouldFailSlugTaken = false, }); @override Future isSlugAvailable(String slug) async { final validation = ClubSlugValidator.validate(slug); if (!validation.isValid) return false; return !takenSlugs.contains(validation.normalizedSlug!); } @override Future createClub({ required String name, required String slug, String description = '', ClubVisibility visibility = ClubVisibility.public, ClubAdmissionPolicy admissionPolicy = ClubAdmissionPolicy.request, String? logoUrl, String? bannerUrl, String? homeRegion, String? motorcycleFocus, String? callerUid, }) async { if (shouldFailSlugTaken || takenSlugs.contains(slug)) { throw const ClubOperationException( 'This club slug is already taken.', code: 'slug-taken', ); } final club = Club( id: 'created_club_123', name: name, slug: slug, description: description, visibility: visibility, admissionPolicy: admissionPolicy, ownerUid: callerUid ?? 'user_1', memberCount: 1, createdAt: DateTime.now(), updatedAt: DateTime.now(), homeRegion: homeRegion, motorcycleFocus: motorcycleFocus, ); lastCreatedClub = club; return club; } } void main() { group('CreateClubScreen Widget Tests', () { testWidgets('renders creation form and handles creation flow', (tester) async { tester.view.physicalSize = const Size(1000, 2000); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); const user = AuthUser(uid: 'alice', isAnonymous: false); final fakeService = FakeCreateClubService(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const CreateClubScreen(), ), ), ); expect(find.text('Create Club'), findsWidgets); expect(find.text('Found a Motorcycle Club'), findsOneWidget); expect(find.text('Club Name *'), findsOneWidget); expect(find.text('Club Handle / Slug *'), findsOneWidget); // Fill in Name await tester.enterText( find.widgetWithText(TextFormField, 'Club Name *'), 'Smoky Mountain MC', ); // Fill in Slug await tester.enterText( find.widgetWithText(TextFormField, 'Club Handle / Slug *'), 'smoky-mountain-mc', ); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); // debounce expect(find.text('Handle is available'), findsOneWidget); final submitButton = find.byKey(const ValueKey('create_club_submit_button')); expect(submitButton, findsOneWidget); await tester.tap(submitButton); await tester.pump(); expect(fakeService.lastCreatedClub, isNotNull); expect(fakeService.lastCreatedClub!.name, equals('Smoky Mountain MC')); expect(fakeService.lastCreatedClub!.slug, equals('smoky-mountain-mc')); }); testWidgets('handles slug-taken conflict gracefully', (tester) async { tester.view.physicalSize = const Size(1000, 2000); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); const user = AuthUser(uid: 'alice', isAnonymous: false); final fakeService = FakeCreateClubService(shouldFailSlugTaken: true); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const CreateClubScreen(), ), ), ); await tester.enterText( find.widgetWithText(TextFormField, 'Club Name *'), 'Contested Name', ); await tester.enterText( find.widgetWithText(TextFormField, 'Club Handle / Slug *'), 'contested-slug', ); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); final submitButton = find.byKey(const ValueKey('create_club_submit_button')); expect(submitButton, findsOneWidget); await tester.tap(submitButton); await tester.pumpAndSettle(); expect( find.text('This handle was just taken by another club. Please pick a different handle.'), findsOneWidget, ); }); testWidgets('renders across responsive viewports and keyboard constraints without overflow', (tester) async { const user = AuthUser(uid: 'alice', isAnonymous: false); final fakeService = FakeCreateClubService(); const viewports = [ Size(412, 915), // Standard portrait Size(915, 412), // Landscape Size(412, 400), // Keyboard-constrained height ]; for (final vp in viewports) { tester.view.physicalSize = vp; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const CreateClubScreen(), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull, reason: 'No layout overflow on $vp'); expect(find.text('Found a Motorcycle Club'), findsOneWidget); expect(find.byType(ListView), findsOneWidget); } }); testWidgets('renders account required screen when unauthenticated', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => null), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const CreateClubScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('Account Required'), findsOneWidget); expect( find.text('Sign in or create a MotorSignal account to join and manage riding clubs.'), findsOneWidget, ); expect(find.text('Sign In / Register'), findsOneWidget); }); testWidgets( 'landscape (800x360) with keyboard inset scrolls to lower fields and submit button, and portrait renders cleanly (#77)', (tester) async { const user = AuthUser(uid: 'alice', isAnonymous: false); final fakeService = FakeCreateClubService(); // 1. Landscape (800x360) with simulated soft keyboard inset tester.view.physicalSize = const Size(800, 360); tester.view.devicePixelRatio = 1.0; tester.view.viewInsets = const FakeViewPadding(bottom: 180); tester.view.padding = const FakeViewPadding(top: 24); addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.view.resetViewInsets); addTearDown(tester.view.resetPadding); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const CreateClubScreen(), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); // Scrollbar should be present for clear scroll affordance in landscape expect(find.byType(Scrollbar), findsOneWidget); // Two-column layout in landscape: Club Name and Club Handle share a row final nameFinder = find.widgetWithText( TextFormField, 'Club Name *', skipOffstage: false, ); final slugFinder = find.widgetWithText( TextFormField, 'Club Handle / Slug *', skipOffstage: false, ); expect(nameFinder, findsOneWidget); expect(slugFinder, findsOneWidget); await tester.ensureVisible(nameFinder); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect( tester.getTopLeft(nameFinder).dx, lessThan(tester.getTopLeft(slugFinder).dx), ); // Enter Club Name & Handle await tester.enterText(nameFinder, 'Canyon Carvers MC'); await tester.enterText(slugFinder, 'canyon-carvers-mc'); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); expect(tester.takeException(), isNull); // Lower fields and submit button remain mounted and scrollable into the visible area above keyboard final regionFinder = find.widgetWithText( TextFormField, 'Home region (optional)', skipOffstage: false, ); final focusFinder = find.widgetWithText( TextFormField, 'Motorcycle focus (optional)', skipOffstage: false, ); final submitFinder = find.byKey( const ValueKey('create_club_submit_button'), skipOffstage: false, ); expect(regionFinder, findsOneWidget); expect(focusFinder, findsOneWidget); expect(submitFinder, findsOneWidget); await tester.ensureVisible(regionFinder); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect( tester.getRect(regionFinder).bottom, lessThanOrEqualTo(360.0 - 180.0 + 1.0), ); await tester.enterText(regionFinder, 'SoCal'); await tester.enterText(focusFinder, 'Sport bikes'); await tester.pump(); // Scroll to and tap the primary submit button while keyboard inset is active await tester.ensureVisible(submitFinder); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect( tester.getRect(submitFinder).bottom, lessThanOrEqualTo(360.0 - 180.0 + 1.0), ); await tester.tap(submitFinder); await tester.pump(); expect(fakeService.lastCreatedClub, isNotNull); expect(fakeService.lastCreatedClub!.name, equals('Canyon Carvers MC')); expect(fakeService.lastCreatedClub!.homeRegion, equals('SoCal')); // 2. Portrait (400x800) with keyboard inset check tester.view.physicalSize = const Size(400, 800); tester.view.viewInsets = const FakeViewPadding(bottom: 280); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }, ); }); }