import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/pantallas/ajustes/widgets/fila_ajuste.dart'; import 'package:pluriwave/tema/pluriwave_theme.dart'; import 'package:pluriwave/widgets/pluri_glass_surface.dart'; /// S8 (Tier 1 visual fidelity): the prototype puts a trailing "current /// value" on nearly every settings row, 13px `rgba(242,247,250,.55)` (t4 /// lines 512-539, 625 — "Voz clara", "Alta", "3 guardados", "Alfabético", /// "7 · 84 MB", "Español", "Hoy, 08:12", "200 MB"). `FilaAjuste` used to /// accept only `icon`/`titulo`/`onTap` — no value slot at all. /// Pre-existing project constraint (see `pantalla_ajustes_test.dart`): /// `PluriGlassSurface` paints a background over `ListTile`'s ink layer, /// which Flutter flags as a warning-level assertion, not a correctness bug. /// Only needed by the `GrupoAjustes` test below — the bare `FilaAjuste` /// tests above never wrap a `ListTile` inside that surface. void _suppressListTileInkAssertion() { final original = FlutterError.onError; FlutterError.onError = (details) { if (details.exceptionAsString().contains( 'ListTile background color or ink splashes may be invisible', )) { return; } original?.call(details); }; addTearDown(() => FlutterError.onError = original); } void main() { Widget host(Widget child) { return MaterialApp( theme: PluriWaveTheme.dark(), home: Scaffold(body: child), ); } testWidgets('renders the trailing value before the chevron when provided', ( tester, ) async { await tester.pumpWidget( host( FilaAjuste( icon: Icons.language_rounded, titulo: 'Language', valor: 'English', onTap: () {}, ), ), ); expect(find.text('English'), findsOneWidget); expect(find.byIcon(Icons.chevron_right_rounded), findsOneWidget); }); testWidgets('renders no trailing value text when valor is omitted ' '(unchanged pre-S8 behaviour)', (tester) async { await tester.pumpWidget( host( FilaAjuste( icon: Icons.info_outline_rounded, titulo: 'Info', onTap: () {}, ), ), ); expect(find.text('Info'), findsOneWidget); expect(find.byIcon(Icons.chevron_right_rounded), findsOneWidget); }); testWidgets( 'visual fidelity (audit 10.6/10.8/10.9): leading icon is 21px, the ' 'title is 14px, and the chevron is 19px at 40% opacity (t4 lines ' '514-515)', (tester) async { await tester.pumpWidget( host( FilaAjuste( icon: Icons.equalizer_rounded, titulo: 'Ecualizador base', onTap: () {}, ), ), ); final icon = tester.widget(find.byIcon(Icons.equalizer_rounded)); expect(icon.size, 21); final titulo = tester.widget(find.text('Ecualizador base')); expect(titulo.style?.fontSize, 14); final chevron = tester.widget( find.byIcon(Icons.chevron_right_rounded), ); expect(chevron.size, 19); expect(chevron.color, const Color(0xFFF2F7FA).withValues(alpha: 0.4)); }, ); testWidgets( 'issue 5 (feedback-pruebas): a long trailing value does not squeeze the ' 'title into wrapping across multiple lines -- the title stays on ONE ' 'line, ellipsizing instead', (tester) async { const titulo = 'Emisora preferida'; const valorLargo = 'Radio Nacional Clasica Internacional FM Stereo HD'; await tester.pumpWidget( host( SizedBox( width: 360, child: FilaAjuste( icon: Icons.radio_rounded, titulo: titulo, valor: valorLargo, onTap: () {}, ), ), ), ); await tester.pump(); expect( tester.takeException(), isNull, reason: 'a squeezed row must not overflow either', ); final tituloWidget = tester.widget(find.text(titulo)); expect( tituloWidget.maxLines, 1, reason: 'issue 5: the title must be constrained to a single line', ); expect(tituloWidget.overflow, TextOverflow.ellipsis); // Measured, not just `find.text` (a wrapped-but-still-present Text // would still satisfy a bare `find.text` match, per the known // "find.text can't catch visual wrap" trap) -- compare the rendered // height against the SAME style/width rendered with a title that is // guaranteed to fit on one line. final alturaConValorLargo = tester.getSize(find.text(titulo)).height; await tester.pumpWidget( host( SizedBox( width: 360, child: FilaAjuste( icon: Icons.radio_rounded, titulo: titulo, onTap: () {}, ), ), ), ); await tester.pump(); final alturaReferencia = tester.getSize(find.text(titulo)).height; expect( alturaConValorLargo, closeTo(alturaReferencia, 1.0), reason: 'the title rendered taller with a long value present -- it ' 'wrapped instead of staying on a single ellipsized line', ); }, ); testWidgets( 'visual fidelity (audit S10): GrupoAjustes insets its row divider by ' '47px, not full-bleed (t4 line 516)', (tester) async { _suppressListTileInkAssertion(); await tester.pumpWidget( host( GrupoAjustes( titulo: 'AUDIO', filas: [ FilaAjuste( icon: Icons.equalizer_rounded, titulo: 'Uno', onTap: () {}, ), FilaAjuste(icon: Icons.hd_rounded, titulo: 'Dos', onTap: () {}), ], ), ), ); final divider = tester.widget(find.byType(Divider)); expect(divider.indent, 47); }, ); testWidgets('visual fidelity (audit 10.5): an explicit iconColor tints the ' 'leading icon (t4 lines 514/516/522 -- equalizer/hd/folder accents)', ( tester, ) async { await tester.pumpWidget( host( FilaAjuste( icon: Icons.equalizer_rounded, titulo: 'Ecualizador base', iconColor: const Color(0xFF21D4D9), onTap: () {}, ), ), ); final icon = tester.widget(find.byIcon(Icons.equalizer_rounded)); expect(icon.color, const Color(0xFF21D4D9)); }); testWidgets( 'visual fidelity (audit 10.5): omitting iconColor keeps the default ' '(unchanged pre-10.5 behaviour)', (tester) async { await tester.pumpWidget( host( FilaAjuste( icon: Icons.language_rounded, titulo: 'Idioma', onTap: () {}, ), ), ); final icon = tester.widget(find.byIcon(Icons.language_rounded)); expect(icon.color, isNull); }, ); testWidgets( 'visual fidelity (audit 10.2): the group eyebrow sits OUTSIDE the ' 'card, at title-tier (20px) padding -- not inside the ' 'PluriGlassSurface (t4 line 511)', (tester) async { _suppressListTileInkAssertion(); await tester.pumpWidget( host( GrupoAjustes( titulo: 'AUDIO', filas: [ FilaAjuste( icon: Icons.equalizer_rounded, titulo: 'Uno', onTap: () {}, ), ], ), ), ); expect( find.ancestor( of: find.text('AUDIO'), matching: find.byType(PluriGlassSurface), ), findsNothing, reason: 'the eyebrow must not be a descendant of the card', ); final eyebrowLeft = tester.getTopLeft(find.text('AUDIO')).dx; expect( eyebrowLeft, 20, reason: 'S5 title tier -- PluriLayout.titleHorizontal', ); }, ); }