FilaAjuste's title Text had no maxLines/overflow, and neither did its trailing current-value Text. An unbounded value (e.g. a real station name in "Emisora preferida") let the trailing Row claim unbounded width, squeezing the title down until it wrapped across several lines that the row's fixed height then cut short. Constrain the title to a single ellipsized line and cap the trailing value's width the same way. FilaAjuste backs all 12 settings rows, so every row is protected, not just the one that happened to expose it.
273 lines
7.9 KiB
Dart
273 lines
7.9 KiB
Dart
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<Icon>(find.byIcon(Icons.equalizer_rounded));
|
|
expect(icon.size, 21);
|
|
|
|
final titulo = tester.widget<Text>(find.text('Ecualizador base'));
|
|
expect(titulo.style?.fontSize, 14);
|
|
|
|
final chevron = tester.widget<Icon>(
|
|
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<Text>(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<Divider>(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<Icon>(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<Icon>(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',
|
|
);
|
|
},
|
|
);
|
|
}
|