Files
pluriwave/test/pantallas/pantalla_bienvenida_test.dart
T
FreeTLab e036f99a61 fix(bienvenida,grabaciones): headline size, CTA radius, storage caption
Screen 14 / audit 14.3+14.7: welcome headline now 34px/ls-1.2 (was
headlineMedium's 28/ls-1.0), CTA restyled to a radius-18 rounded
rectangle instead of Material 3's default StadiumBorder (t4:696,715).

Screen 12 / audit 12.2+12.3: the recordings storage card now shows the
bold "X of Y used" headline ABOVE a 6px/radius-3 bar, followed by a
real folder-path + purge-policy caption below it -- was the bar first
with the "used" string as its only (small, generic) caption (t4:613).
New ARB key recordingsLibraryStorageFolderCaption, translated to all
13 locales.

Item 12.5 (folder/max-size settings living behind the header action
instead of inline on this screen) is a deliberate structural split
from WU15b; the audit itself scopes it out of this pass.
2026-07-30 16:00:30 +02:00

299 lines
10 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(
'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<Text>(
find.textContaining('53.412 emisoras de 238 países'),
);
expect(body.style?.fontSize, 14.5);
final tituloBullet = tester.widget<Text>(
find.text(l10n.welcomeBullet1Title),
);
expect(tituloBullet.style?.fontSize, 13.5);
expect(tituloBullet.style?.fontWeight, FontWeight.w800);
final subtituloBullet = tester.widget<Text>(
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<ClipRRect>(
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<Text>(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<FilledButton>(find.byType(FilledButton));
final forma = boton.style?.shape?.resolve(<WidgetState>{});
expect(
forma,
isA<RoundedRectangleBorder>().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<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',
);
},
);
}