diff --git a/lib/widgets/mini_reproductor.dart b/lib/widgets/mini_reproductor.dart index 5d6c6c1..b9feac6 100644 --- a/lib/widgets/mini_reproductor.dart +++ b/lib/widgets/mini_reproductor.dart @@ -129,20 +129,31 @@ class _MiniReproductorState extends State { EstadoReproduccion.detenido; final activo = s == EstadoReproduccion.reproduciendo; + // WU16: reconectando/error are + // "connectivity trouble" states — + // tinted with offlineAccent so they + // read as visually distinct from + // ordinary loading/paused/stopped. + final conexionEnProblema = + s == + EstadoReproduccion.reconectando || + s == EstadoReproduccion.error; return Text( _labelEstado(l10n, s), style: Theme.of( context, ).textTheme.bodySmall?.copyWith( color: - activo + conexionEnProblema + ? t.offlineAccent + : activo ? t.warmCoral : Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.7), fontWeight: - activo + (activo || conexionEnProblema) ? FontWeight.w600 : FontWeight.w400, ), @@ -173,12 +184,20 @@ class _MiniReproductorState extends State { // cargando (spinner), never as the error/retry affordance. if (s == EstadoReproduccion.cargando || s == EstadoReproduccion.reconectando) { - return const SizedBox( + // WU16: only the reconectando sub-state gets the offline + // accent — plain cargando (e.g. the very first play) + // keeps the default spinner colour, since it is not a + // connectivity problem. + final reconectando = s == EstadoReproduccion.reconectando; + return SizedBox( width: 48, height: 48, child: Padding( - padding: EdgeInsets.all(12), - child: CircularProgressIndicator(strokeWidth: 2), + padding: const EdgeInsets.all(12), + child: CircularProgressIndicator( + strokeWidth: 2, + color: reconectando ? t.offlineAccent : null, + ), ), ); } @@ -187,7 +206,7 @@ class _MiniReproductorState extends State { final emisoraActual = estado.emisoraActual; return IconButton( tooltip: l10n.retryAction, - icon: const Icon(Icons.refresh_rounded), + icon: Icon(Icons.refresh_rounded, color: t.offlineAccent), onPressed: emisoraActual != null ? () => estado.reproducir(emisoraActual) diff --git a/openspec/changes/rediseno-funcional/tasks.md b/openspec/changes/rediseno-funcional/tasks.md index 79e511f..f47ca05 100644 --- a/openspec/changes/rediseno-funcional/tasks.md +++ b/openspec/changes/rediseno-funcional/tasks.md @@ -971,7 +971,7 @@ real capability living in the header, not decorative chrome). - [x] 15b.6 Verify — scoped suite green: 605/605 (up from 604), 2 skipped (unchanged, both pre-existing WU15 documented skips). `flutter analyze`: 1 issue, identical to baseline. Literal-encoding scan: zero hits. - +## WU16 — Connectivity banner restyle (offline / reconnect) **Commit**: `feat(connectivity): restyle offline and reconnect banners` **Depends on**: WU1 @@ -979,16 +979,31 @@ real capability living in the header, not decorative chrome). **Verify**: `flutter test test/widgets/reconnect_ui_test.dart test/servicios/servicio_audio_reconnect_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')` **Modified tests**: `reconnect_ui_test.dart`. `servicio_audio_reconnect_test.dart` must pass unmodified. -- [ ] 16.1 Verify-first — run `servicio_audio_reconnect_test.dart` and inspect the reconnect controller: confirm - whether it tracks an attempt count. -- [ ] 16.2 RED — restyle test for the offline banner (visual only) and, **only if 16.1 confirms an attempt count - exists**, a test for the "Reconectando · intento N de M" label. -- [ ] 16.3 GREEN — restyle the offline banner to the new visual language. -- [ ] 16.4 GREEN — **conditionally**: wire the attempt-count label only if the controller exposes one; otherwise - ship the restyle without the counter — do not add new plumbing to invent one. -- [ ] 16.5 REFACTOR — confirm shimmer skeletons (`TarjetaEmisoraShimmer`) are unchanged. -- [ ] 16.6 Verify — `reconnect_ui_test.dart` updated and green; `servicio_audio_reconnect_test.dart` unmodified and - green. +- [x] 16.1 Verify-first — run `servicio_audio_reconnect_test.dart` and inspect the reconnect controller: confirm + whether it tracks an attempt count. **Result: it does NOT reach the UI.** `ControladorReconexion.intentos` + exists as a getter, but `ServicioAudio` (`lib/servicios/servicio_audio.dart`) only ever reads it inside a + `developer.log` debug line (line 413) — its public `estadoStream` maps every playback state to the + `EstadoReproduccion` enum (no attempt-count payload) and `_handler.reconectando` is a bare bool. Wiring a + count to the UI would require adding a getter/stream to `servicio_audio.dart`, which is one of the four + files this change must keep at an **empty git diff vs `main`** — structurally blocked, not just untested. + Shipping without the counter per 16.4, exactly as the risk register anticipated. +- [x] 16.2 RED — 3 new `testWidgets` cases added to `reconnect_ui_test.dart` (no attempt-count test, per 16.1): + reconnecting tints the spinner + status label with `offlineAccent`; plain `cargando` keeps the default + spinner colour (regression guard proving the tint is reconnect-specific, not blanket-loading); error tints + the retry icon + status label with `offlineAccent`. Confirmed RED: 2 of the 3 failed (`Actual: `) + against the pre-restyle widget. +- [x] 16.3 GREEN — `lib/widgets/mini_reproductor.dart`: the status-label `Text` and the reconnect spinner / + error retry icon now read `context.pluriTokens.offlineAccent` (WU1's previously-unused token, whose own + doc comment already named it for this WU) whenever the stream reports `reconectando` or `error`. Plain + `cargando` explicitly keeps `color: null` (the theme default) — verified by 16.2's regression test. +- [x] 16.4 GREEN — conditional step confirmed moot: no attempt-count label added, no new plumbing introduced. +- [x] 16.5 REFACTOR — `TarjetaEmisoraShimmer` (`lib/widgets/tarjeta_emisora.dart`, consumed by + `pantalla_buscar.dart`) is untouched by this commit — confirmed via `git diff --stat`, zero lines. +- [x] 16.6 Verify — scoped suite green: 16/16 (5 `reconnect_ui_test.dart` + 8 `servicio_audio_reconnect_test.dart` + [byte-identical, unmodified] + 3 `mini_reproductor_configurar_test.dart` re-run as an adjacent-file + regression check). `flutter analyze`: 1 issue, identical to baseline. Scoped `dart format`: reformatted the + hand-written production file once (whitespace only), stable on re-run. Literal-encoding scan: zero hits on + the 2 touched files. ## WU17 — Welcome / onboarding screen diff --git a/test/widgets/reconnect_ui_test.dart b/test/widgets/reconnect_ui_test.dart index 7307e43..a4b9d5e 100644 --- a/test/widgets/reconnect_ui_test.dart +++ b/test/widgets/reconnect_ui_test.dart @@ -3,6 +3,7 @@ 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/servicios/servicio_audio.dart'; +import 'package:pluriwave/tema/pluriwave_tokens.dart'; import 'package:pluriwave/widgets/mini_reproductor.dart'; import 'package:provider/provider.dart'; @@ -93,4 +94,120 @@ void main() { expect(find.byType(AlertDialog), findsNothing); expect(find.byIcon(Icons.refresh_rounded), findsOneWidget); }); + + // WU16: the offline/reconnect banner restyle. `offlineAccent` (added in + // WU1, previously unused by any screen) tints the connectivity-trouble + // sub-states so they read as visually distinct from ordinary loading. + testWidgets( + 'reconnecting state tints the spinner and status label with the offline accent', + (tester) async { + final audio = FakeServicioAudio(); + final estado = _estadoRadio(audio); + addTearDown(estado.dispose); + await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno')); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: estado, + child: MaterialApp( + locale: const Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: MiniReproductor()), + ), + ), + ); + await tester.pumpAndSettle(); + + audio.emitirEstado(EstadoReproduccion.reconectando); + await tester.pump(); + await tester.pump(); + + expect( + tester + .widget( + find.byType(CircularProgressIndicator), + ) + .color, + PluriWaveTokens.dark.offlineAccent, + ); + expect( + tester.widget(find.text('Reconectando...')).style?.color, + PluriWaveTokens.dark.offlineAccent, + ); + }, + ); + + testWidgets( + 'plain buffering keeps the default spinner colour, not the offline accent', + (tester) async { + final audio = FakeServicioAudio(); + final estado = _estadoRadio(audio); + addTearDown(estado.dispose); + await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno')); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: estado, + child: MaterialApp( + locale: const Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: MiniReproductor()), + ), + ), + ); + await tester.pumpAndSettle(); + + audio.emitirEstado(EstadoReproduccion.cargando); + await tester.pump(); + await tester.pump(); + + expect( + tester + .widget( + find.byType(CircularProgressIndicator), + ) + .color, + isNull, + reason: 'cargando is not a connectivity problem, keep the default', + ); + }, + ); + + testWidgets( + 'error state tints the retry icon and status label with the offline accent', + (tester) async { + final audio = FakeServicioAudio(); + final estado = _estadoRadio(audio); + addTearDown(estado.dispose); + await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno')); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: estado, + child: MaterialApp( + locale: const Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: MiniReproductor()), + ), + ), + ); + await tester.pumpAndSettle(); + + audio.emitirEstado(EstadoReproduccion.error); + await tester.pump(); + await tester.pump(); + + expect( + tester.widget(find.byIcon(Icons.refresh_rounded)).color, + PluriWaveTokens.dark.offlineAccent, + ); + expect( + tester.widget(find.text('Error de conexión')).style?.color, + PluriWaveTokens.dark.offlineAccent, + ); + }, + ); }