Audit 14.2 (t4 lines 693/706): content was top-anchored in a plain SingleChildScrollView; the prototype anchors it to the bottom with two flexible spacers, so the CTA sits at the screen edge. Restructured as a scrollable Expanded region (safe on short screens / large text scale) with the CTA pinned below it, always at the bottom. Audit 14.1 (t4 lines 687-689): added the full-bleed blurred banner backdrop, reusing aurora_wave_banner.png (the same asset the deleted PluriScreenHeader used) since this app has no bundled equivalent of the prototype's mockup-only banner.jpg. No monetization content touched (PRO pill, "14 días", pricing card, free-version link all remain absent per the binding no-monetization decision).
195 lines
6.7 KiB
Dart
195 lines
6.7 KiB
Dart
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<EstadoNavegacionRaiz>.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('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<Text>(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<NavigatorState>();
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<EstadoNavegacionRaiz>.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<void>(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',
|
|
);
|
|
},
|
|
);
|
|
}
|