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.
285 lines
9.4 KiB
Dart
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',
|
|
);
|
|
},
|
|
);
|
|
}
|