Files
pluriwave/test/widgets/pluri_bottom_navigation_test.dart
T
FreeTLab 4537497e83 fix(chrome): rebuild MiniReproductor as a full-bleed opaque bar with art
Item 22 / audit 3.6 (t4:184-188): the mini player was a floating
999-radius glass pill with no artwork. Replace it with a 60px opaque
bar (listSurface at .97 alpha), full-bleed edge to edge, showing the
station's square 42x42 artwork instead of the abstract playing-bars
indicator. app.dart no longer wraps the bar in the balloon nav's own
8px side margin, so it now spans the full width.

MiniReproductor.altura is re-measured (72 -> 60) now that the bar's
content height is fixed by construction; PluriLayout.bottomChromeInset
derives from it as before. Both the S3-R3 configurarLocalizaciones
guard and the altura measurement test still pass unmodified.
2026-07-30 10:38:59 +02:00

285 lines
9.4 KiB
Dart

import 'dart:ui' show Tristate;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/tema/pluriwave_theme.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:pluriwave/widgets/mini_reproductor.dart';
import 'package:pluriwave/widgets/pluri_bottom_navigation.dart';
import 'package:pluriwave/widgets/pluri_icon.dart';
import 'package:pluriwave/widgets/pluri_layout.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes.dart';
/// "Barra globo" bottom navigation — Design turn t4, option 4a. Spec
/// transcribed verbatim from `PluriWave Rediseno.dc.html` lines 91-99: a
/// solid capsule bar with a taller balloon capsule that slides beneath
/// whichever tab is active.
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
const items = [
PluriNavItem(glyph: PluriIconGlyph.home, label: 'Escuchar'),
PluriNavItem(glyph: PluriIconGlyph.search, label: 'Buscar'),
PluriNavItem(glyph: PluriIconGlyph.favorites, label: 'Favoritos'),
PluriNavItem(glyph: PluriIconGlyph.alarm, label: 'Alarmas'),
PluriNavItem(glyph: PluriIconGlyph.settings, label: 'Ajustes'),
];
Widget hostFor(int selectedIndex, {ValueChanged<int>? onSelected}) {
return MaterialApp(
theme: PluriWaveTheme.dark(),
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: SizedBox(
width: 390,
child: PluriBottomNavigation(
items: items,
selectedIndex: selectedIndex,
onSelected: onSelected ?? (_) {},
),
),
),
);
}
testWidgets('only the active tab renders a visible text label', (
tester,
) async {
await tester.pumpWidget(hostFor(1));
await tester.pump();
expect(find.text('Buscar'), findsOneWidget);
expect(find.text('Escuchar'), findsNothing);
expect(find.text('Favoritos'), findsNothing);
expect(find.text('Alarmas'), findsNothing);
expect(find.text('Ajustes'), findsNothing);
});
testWidgets('active icon and label use the brand colour (#21D4D9)', (
tester,
) async {
await tester.pumpWidget(hostFor(2));
await tester.pump();
final activeIcon = tester.widget<PluriIcon>(
find.descendant(
of: find.byKey(PluriBottomNavigation.itemKey(2)),
matching: find.byType(PluriIcon),
),
);
expect(activeIcon.color, PluriWaveTokens.brand);
final label = tester.widget<Text>(find.text('Favoritos'));
expect(label.style?.color, PluriWaveTokens.brand);
});
testWidgets('inactive icons are dimmed to 46% and carry no colour override', (
tester,
) async {
await tester.pumpWidget(hostFor(0));
await tester.pump();
final inactiveIcon = tester.widget<PluriIcon>(
find.descendant(
of: find.byKey(PluriBottomNavigation.itemKey(1)),
matching: find.byType(PluriIcon),
),
);
expect(inactiveIcon.color, isNull);
final dimmed = tester.widget<Opacity>(
find.ancestor(
of: find.descendant(
of: find.byKey(PluriBottomNavigation.itemKey(1)),
matching: find.byType(PluriIcon),
),
matching: find.byType(Opacity),
),
);
expect(dimmed.opacity, closeTo(0.46, 0.001));
});
testWidgets(
'the balloon exists, sized per spec, and centred over the active tab',
(tester) async {
await tester.pumpWidget(hostFor(0));
await tester.pump();
final balloon = find.byKey(PluriBottomNavigation.balloonKey);
expect(balloon, findsOneWidget);
expect(tester.getSize(balloon), const Size(110, 74));
final balloonCenterX = tester.getCenter(balloon).dx;
final tabCenterX =
tester.getCenter(find.byKey(PluriBottomNavigation.itemKey(0))).dx;
expect(balloonCenterX, closeTo(tabCenterX, 1));
final decoratedBox = tester.widget<DecoratedBox>(
find.descendant(of: balloon, matching: find.byType(DecoratedBox)),
);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.color, PluriWaveTokens.dark.balloonSurface);
},
);
testWidgets('selection changes slide the balloon to the newly active tab', (
tester,
) async {
var selected = 0;
await tester.pumpWidget(
StatefulBuilder(
builder:
(context, setState) => MaterialApp(
theme: PluriWaveTheme.dark(),
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: SizedBox(
width: 390,
child: PluriBottomNavigation(
items: items,
selectedIndex: selected,
onSelected: (i) => setState(() => selected = i),
),
),
),
),
),
);
await tester.pump();
final initialX =
tester.getCenter(find.byKey(PluriBottomNavigation.balloonKey)).dx;
await tester.tap(find.byKey(PluriBottomNavigation.itemKey(3)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 260));
final finalX =
tester.getCenter(find.byKey(PluriBottomNavigation.balloonKey)).dx;
expect(finalX, isNot(closeTo(initialX, 1)));
expect(
finalX,
closeTo(
tester.getCenter(find.byKey(PluriBottomNavigation.itemKey(3))).dx,
1,
),
);
});
testWidgets('inactive tabs keep their semantic label without visible text', (
tester,
) async {
final semantics = tester.ensureSemantics();
await tester.pumpWidget(hostFor(0));
await tester.pump();
final node = tester.getSemantics(
find.byKey(PluriBottomNavigation.itemKey(3)),
);
expect(node.label, 'Alarmas');
expect(node.flagsCollection.isButton, isTrue);
expect(node.flagsCollection.isSelected, Tristate.isFalse);
semantics.dispose();
});
testWidgets('every tab meets the 48x48dp minimum tap target', (tester) async {
await tester.pumpWidget(hostFor(0));
await tester.pump();
for (var i = 0; i < items.length; i++) {
final size = tester.getSize(find.byKey(PluriBottomNavigation.itemKey(i)));
expect(size.width, greaterThanOrEqualTo(48));
expect(size.height, greaterThanOrEqualTo(48));
}
});
testWidgets('PluriBottomNavigation.altura matches its real laid-out height', (
tester,
) async {
await tester.pumpWidget(hostFor(0));
await tester.pump();
final real = tester.getSize(find.byType(PluriBottomNavigation)).height;
expect(PluriBottomNavigation.altura, closeTo(real, 0.5));
});
testWidgets(
'PluriLayout.bottomChromeInset matches the real composed chrome height '
'(MiniReproductor + PluriBottomNavigation + the safe-area gap)',
(tester) async {
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await estado.reproducir(emisoraDemo(uuid: 'a', nombre: 'Station A'));
const chromeKey = Key('chromeSafeArea');
await tester.pumpWidget(
ChangeNotifierProvider<EstadoRadio>.value(
value: estado,
child: MaterialApp(
theme: PluriWaveTheme.dark(),
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: const SizedBox.shrink(),
bottomNavigationBar: SafeArea(
key: chromeKey,
top: false,
minimum: const EdgeInsets.only(bottom: PluriLayout.compactGap),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Mirrors app.dart's real composition: item 22 / audit
// 3.6 made MiniReproductor full-bleed, so only
// PluriBottomNavigation keeps the 8px side margin.
const MiniReproductor(),
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: PluriBottomNavigation(
items: items,
selectedIndex: 0,
onSelected: (_) {},
),
),
],
),
),
),
),
),
);
await tester.pump();
final medido = tester.getSize(find.byKey(chromeKey)).height;
expect(
PluriLayout.bottomChromeInset,
closeTo(medido, 4),
reason:
'bottomChromeInset must be derived from the real composed chrome '
'height (MiniReproductor + PluriBottomNavigation + the '
'safe-area gap), not guessed — if this fails, re-measure and '
'update the formula in pluri_layout.dart',
);
},
);
}