import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_navegacion.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/pantallas/pantalla_bienvenida.dart'; import 'package:provider/provider.dart'; /// WU17: `onboarding-welcome` spec — full-screen route (not a modal), /// content only (logo/headline/body/3 bullets/1 CTA), binding /// no-monetization guard, and CTA dismisses to Escuchar with the route /// popped. Widget _app({required EstadoNavegacionRaiz navegacion}) { return ChangeNotifierProvider.value( value: navegacion, child: const MaterialApp( locale: Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: PantallaBienvenida(), ), ); } void main() { testWidgets( 'renders as a full-screen route with a logo, headline, body copy, ' 'exactly 3 feature bullets, and exactly one primary CTA', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); // A full-screen route, never a modal dialog. expect(find.byType(Dialog), findsNothing); expect(find.byType(AlertDialog), findsNothing); expect(find.byType(Scaffold), findsOneWidget); // Audit 14.1 adds a second Image (the blurred backdrop), so "the // logo mark" is now asserted by key rather than "the only Image". expect( find.byKey(const ValueKey('welcome-logo')), findsOneWidget, reason: 'the logo mark', ); expect(find.text('Tu mundo, en directo'), findsOneWidget); expect( find.textContaining('53.412 emisoras de 238 países'), findsOneWidget, ); expect( find.byType(FilaCaracteristicaBienvenida), findsNWidgets(3), reason: 'exactly 3 feature bullets', ); expect( find.byType(FilledButton), findsOneWidget, reason: 'exactly 1 primary CTA', ); expect(find.text('Empezar a escuchar'), findsOneWidget); }, ); testWidgets( 'visual fidelity (audit 14.4/14.5): body copy is 14.5px, bullet titles ' 'are 13.5px/w800, bullet subtitles are 11.5px (t4 lines 697/700-702)', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final l10n = lookupAppLocalizations(const Locale('es')); final body = tester.widget( find.textContaining('53.412 emisoras de 238 países'), ); expect(body.style?.fontSize, 14.5); final tituloBullet = tester.widget( find.text(l10n.welcomeBullet1Title), ); expect(tituloBullet.style?.fontSize, 13.5); expect(tituloBullet.style?.fontWeight, FontWeight.w800); final subtituloBullet = tester.widget( find.text(l10n.welcomeBullet1Subtitle), ); expect(subtituloBullet.style?.fontSize, 11.5); }, ); testWidgets( 'visual fidelity (audit 14.6): the 3 feature bullets are spaced 12px ' 'apart, not 16 (t4 line 701)', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final bullets = find.byType(FilaCaracteristicaBienvenida); final gap = tester.getTopLeft(bullets.at(1)).dy - tester.getBottomLeft(bullets.at(0)).dy; expect(gap, 12); }, ); testWidgets( 'visual fidelity (audit 14.8): the logo mark is clipped to a 20px ' 'rounded rect, not painted square (t4 line 693)', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final clip = tester.widget( find.ancestor( of: find.byKey(const ValueKey('welcome-logo')), matching: find.byType(ClipRRect), ), ); expect(clip.borderRadius, BorderRadius.circular(20)); }, ); testWidgets('visual fidelity (audit 14.3): the headline is 34px/w800/ls-1.2 ' '(t4 line 696), not headlineMedium\'s 28', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final headline = tester.widget(find.text('Tu mundo, en directo')); expect(headline.style?.fontSize, 34); expect(headline.style?.fontWeight, FontWeight.w800); expect(headline.style?.letterSpacing, -1.2); }); testWidgets('visual fidelity (audit 14.7): the CTA is a radius-18 rounded ' 'rectangle (t4 line 715), not the default fully-round StadiumBorder', ( tester, ) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final boton = tester.widget(find.byType(FilledButton)); final forma = boton.style?.shape?.resolve({}); expect( forma, isA().having( (s) => (s.borderRadius as BorderRadius).topLeft, 'topLeft radius', const Radius.circular(18), ), ); }); testWidgets('rendered content contains no monetization strings', ( tester, ) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final textoCompleto = tester .widgetList(find.byType(Text)) .map((widget) => widget.data ?? '') .join('\n'); expect(textoCompleto.contains('PRO'), isFalse); expect(textoCompleto.contains('€'), isFalse); expect(textoCompleto.contains(r'$'), isFalse); expect( RegExp(r'\d+\s*d[ií]as?\b', caseSensitive: false).hasMatch(textoCompleto), isFalse, reason: 'no day-count trial phrase (e.g. "14 días")', ); expect( find.textContaining('gratuita', findRichText: true), findsNothing, reason: 'no secondary "free version" link', ); expect( find.textContaining('gratis', findRichText: true), findsNothing, reason: 'no secondary "free version" link', ); // Only one tappable action on the whole screen: the primary CTA. expect(find.byType(TextButton), findsNothing); expect(find.byType(FilledButton), findsOneWidget); }); testWidgets( 'tapping the primary CTA switches to Escuchar and pops the welcome route', (tester) async { // Start on a DIFFERENT tab so a pass actually proves irA() ran, // rather than coasting on EstadoNavegacionRaiz's own escuchar default. final navegacion = EstadoNavegacionRaiz()..irA(RaizPluriWave.ajustes); addTearDown(navegacion.dispose); final navigatorKey = GlobalKey(); await tester.pumpWidget( ChangeNotifierProvider.value( value: navegacion, child: MaterialApp( navigatorKey: navigatorKey, locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: Center(child: Text('home stand-in'))), ), ), ); unawaited( navigatorKey.currentState!.push( MaterialPageRoute(builder: (_) => const PantallaBienvenida()), ), ); await tester.pumpAndSettle(); expect(find.byType(PantallaBienvenida), findsOneWidget); await tester.tap(find.text('Empezar a escuchar')); await tester.pumpAndSettle(); expect(navegacion.actual, RaizPluriWave.escuchar); expect( find.byType(PantallaBienvenida), findsNothing, reason: 'the welcome route was removed from the back stack', ); expect(find.text('home stand-in'), findsOneWidget); }, ); testWidgets('visual fidelity (audit 14.1, proto t4 lines 687-689): a blurred ' 'banner backdrop is present behind the content', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); expect( find.byKey(const ValueKey('welcome-background-art')), findsOneWidget, reason: 'the prototype draws a blurred, 40%-opacity banner image ' 'full-bleed behind the content — this screen used to be a ' 'plain Scaffold with no backdrop at all', ); }); testWidgets( 'visual fidelity (audit 14.2, proto t4 lines 693/706): the primary CTA ' 'is anchored to the bottom edge, not immediately after the bullets', (tester) async { final navegacion = EstadoNavegacionRaiz(); addTearDown(navegacion.dispose); tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(_app(navegacion: navegacion)); await tester.pumpAndSettle(); final alturaPantalla = tester.view.physicalSize.height; final ctaAbajo = tester.getBottomLeft(find.byType(FilledButton)).dy; expect( ctaAbajo, greaterThan(alturaPantalla - 100), reason: 'the prototype anchors content to the bottom with two ' 'flexible spacers (lines 693, 706) so the CTA sits at the ' 'screen edge — it used to render immediately after the top ' 'padding instead', ); }, ); }