fix(visualizador): add a discrete-bar rendering mode
Audit 1.7/2.5 (t4 lines 66-68, 120-122): the prototype draws 30 discrete bottom-anchored bars (radius 2, vertical gradient); the build's VisualizadorAudio painted a single continuous oscilloscope stroke, so its `barras: 30` parameter never produced bars. Adds an opt-in `barrasDiscretas` mode (default false, byte-identical continuous rendering preserved for any other caller) plus a `gradienteFinAlpha` knob for the two screens' differing gradient end-alpha (.3 vs .45). Wires it into the Escuchar hero and the full player, correcting the player's bar count/height to match the prototype (26->30 bars, 46->40px) at the same time.
This commit is contained in:
@@ -10,6 +10,7 @@ import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_inicio.dart';
|
||||
import 'package:pluriwave/widgets/visualizador_audio.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -208,6 +209,51 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'visual fidelity (audit 1.7): the hero visualizer renders 30 discrete '
|
||||
'bars, not the continuous waveform stroke',
|
||||
(tester) async {
|
||||
// Item 16 is exactly the widget the audit warned about
|
||||
// (visualizador_audio.dart:77, `_controller.repeat()`) — bounded pump
|
||||
// only, never pumpAndSettle.
|
||||
_setLargeSurfaceSize(tester);
|
||||
final favoritos = FakeServicioFavoritos();
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: favoritos,
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadio(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno');
|
||||
await favoritos.agregar(sonando);
|
||||
await estado.cargarFavoritos();
|
||||
await tester.runAsync(() => estado.reproducir(sonando));
|
||||
|
||||
await tester.pumpWidget(
|
||||
_conProviders(estado, _testApp(const PantallaInicio())),
|
||||
);
|
||||
await _pumpBounded(tester);
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('visualizador-barra-0')),
|
||||
findsOneWidget,
|
||||
reason: 'prototype t4 line 66-68: 30 discrete bottom-anchored bars',
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(VisualizadorAudio),
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'visual fidelity (audit 1.6): a station missing every meta field omits '
|
||||
'the line gracefully, no stray separators',
|
||||
|
||||
@@ -412,6 +412,35 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'visual fidelity (audit 2.5): the visualizer renders 30 discrete '
|
||||
'bars, not the continuous waveform stroke',
|
||||
(tester) async {
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await montarPantalla(tester, estado);
|
||||
|
||||
final visualizador = find.byKey(const Key('player-visualizer'));
|
||||
expect(visualizador, findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: visualizador,
|
||||
matching: find.byKey(const Key('visualizador-barra-0')),
|
||||
),
|
||||
findsOneWidget,
|
||||
reason: 'prototype t4 line 120-122: 30 discrete bars',
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: visualizador,
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'exactly 4 tool-tray tiles render: EQ propio, Grabar, sleep timer, Compartir',
|
||||
(tester) async {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:pluriwave/widgets/visualizador_audio.dart';
|
||||
|
||||
/// Audit 1.7/2.5 (t4 lines 66-68, 120-122): the prototype draws 30 discrete
|
||||
/// bottom-anchored bars (`border-radius:2px`, a top-to-bottom gradient), but
|
||||
/// `_WaveFlowPainter` (`visualizador_audio.dart:206-275`) paints a single
|
||||
/// continuous oscilloscope stroke — the `barras` parameter was inert. These
|
||||
/// guards assert the actual bar COUNT and that the render is structurally
|
||||
/// discrete (no `CustomPaint` stroke), not just "looks bar-ish".
|
||||
void main() {
|
||||
group('VisualizadorAudio.barrasDiscretas', () {
|
||||
testWidgets(
|
||||
'renders exactly `barras` discrete bars, not a single CustomPaint path',
|
||||
(tester) async {
|
||||
final controller = StreamController<EstadoReproduccion>.broadcast();
|
||||
addTearDown(controller.close);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: VisualizadorAudio(
|
||||
estadoStream: controller.stream,
|
||||
barras: 30,
|
||||
barrasDiscretas: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
for (var i = 0; i < 30; i++) {
|
||||
expect(
|
||||
find.byKey(Key('visualizador-barra-$i')),
|
||||
findsOneWidget,
|
||||
reason: 'bar $i of 30 must exist (t4 lines 66-68: 30 bars)',
|
||||
);
|
||||
}
|
||||
expect(
|
||||
find.byKey(const Key('visualizador-barra-30')),
|
||||
findsNothing,
|
||||
reason: 'exactly 30 bars, not 31+',
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(VisualizadorAudio),
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
findsNothing,
|
||||
reason:
|
||||
'discrete mode must not fall back to the continuous '
|
||||
'_WaveFlowPainter stroke',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('each bar has radius 2 and a top-to-bottom gradient ending '
|
||||
'at the given alpha (t4 line 67: linear-gradient(180deg,#7EE4C2,'
|
||||
'rgba(126,228,194,.3)))', (tester) async {
|
||||
final controller = StreamController<EstadoReproduccion>.broadcast();
|
||||
addTearDown(controller.close);
|
||||
const color = Color(0xFF7EE4C2);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: VisualizadorAudio(
|
||||
estadoStream: controller.stream,
|
||||
barras: 8,
|
||||
barrasDiscretas: true,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final barra = tester.widget<Container>(
|
||||
find.byKey(const Key('visualizador-barra-0')),
|
||||
);
|
||||
final decoracion = barra.decoration as BoxDecoration;
|
||||
expect(
|
||||
decoracion.borderRadius,
|
||||
BorderRadius.circular(2),
|
||||
reason: 't4 line 67: border-radius:2px',
|
||||
);
|
||||
final gradiente = decoracion.gradient as LinearGradient;
|
||||
expect(gradiente.begin, Alignment.topCenter);
|
||||
expect(gradiente.end, Alignment.bottomCenter);
|
||||
expect(gradiente.colors.first, color);
|
||||
expect(gradiente.colors.last, color.withValues(alpha: 0.3));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'a custom gradienteFinAlpha changes only the gradient END colour '
|
||||
'(t4 line 121: the player uses rgba(244,184,96,.45), not .3)',
|
||||
(tester) async {
|
||||
final controller = StreamController<EstadoReproduccion>.broadcast();
|
||||
addTearDown(controller.close);
|
||||
const color = Color(0xFFF4B860);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: VisualizadorAudio(
|
||||
estadoStream: controller.stream,
|
||||
barras: 8,
|
||||
barrasDiscretas: true,
|
||||
color: color,
|
||||
gradienteFinAlpha: 0.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final barra = tester.widget<Container>(
|
||||
find.byKey(const Key('visualizador-barra-0')),
|
||||
);
|
||||
final gradiente =
|
||||
(barra.decoration as BoxDecoration).gradient as LinearGradient;
|
||||
expect(gradiente.colors.last, color.withValues(alpha: 0.45));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('barrasDiscretas defaults to false — existing callers keep the '
|
||||
'continuous CustomPaint stroke, no regression', (tester) async {
|
||||
final controller = StreamController<EstadoReproduccion>.broadcast();
|
||||
addTearDown(controller.close);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: VisualizadorAudio(estadoStream: controller.stream),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(VisualizadorAudio),
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const Key('visualizador-barra-0')), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user