feat(ui): add premium PluriWave redesign
Build & Deploy Pluriwave / Análisis de código (push) Failing after 21s
Build & Deploy Pluriwave / Build APK + AAB release (push) Has been skipped

This commit is contained in:
2026-05-20 18:42:22 +02:00
parent f95a8290ae
commit c707fc9911
30 changed files with 2218 additions and 954 deletions
+35
View File
@@ -171,6 +171,41 @@ void main() {
expect(estado.presetEcualizador, principal);
expect(audio.presetsAplicados.last, principal);
});
test('reordenar favoritos reindexa de forma determinística', () async {
final favoritos = FakeServicioFavoritos();
await favoritos.agregar(emisoraDemo(uuid: 'a', nombre: 'A'));
await favoritos.agregar(emisoraDemo(uuid: 'b', nombre: 'B'));
await favoritos.agregar(emisoraDemo(uuid: 'c', nombre: 'C'));
await favoritos.reordenar('a', 2);
final lista = await favoritos.obtenerTodos();
expect(lista.map((e) => e.uuid).toList(), equals(['b', 'c', 'a']));
expect(lista.map((e) => e.orden).toList(), equals([0, 1, 2]));
});
test('toggleFavorito refresca lista global y evita estado stale', () async {
final favoritos = FakeServicioFavoritos();
final emisora = emisoraDemo(uuid: 'fav-sync', nombre: 'Sync');
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(populares: [emisora]),
servicioEcualizador: FakeServicioEcualizador(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
await estado.inicializar();
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isFalse);
await estado.toggleFavorito(emisora);
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isTrue);
await estado.toggleFavorito(emisora);
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isFalse);
});
});
}
+26 -8
View File
@@ -55,35 +55,53 @@ class FakeServicioAudio extends ServicioAudio {
}
class FakeServicioFavoritos extends ServicioFavoritos {
final Map<String, Emisora> _favoritos = {};
final List<Emisora> _favoritos = [];
int toggleCalls = 0;
@override
Future<List<Emisora>> obtenerTodos() async => _favoritos.values.toList();
Future<List<Emisora>> obtenerTodos() async =>
_favoritos.map((e) => e.copyWith()).toList();
@override
Future<void> agregar(Emisora emisora) async {
_favoritos[emisora.uuid] = emisora;
_favoritos.removeWhere((e) => e.uuid == emisora.uuid);
_favoritos.add(emisora.copyWith(orden: _favoritos.length));
}
@override
Future<void> eliminar(String uuid) async {
_favoritos.remove(uuid);
_favoritos.removeWhere((e) => e.uuid == uuid);
for (var i = 0; i < _favoritos.length; i++) {
_favoritos[i] = _favoritos[i].copyWith(orden: i);
}
}
@override
Future<bool> esFavorito(String uuid) async => _favoritos.containsKey(uuid);
Future<bool> esFavorito(String uuid) async =>
_favoritos.any((e) => e.uuid == uuid);
@override
Future<bool> toggleFavorito(Emisora emisora) async {
toggleCalls += 1;
if (_favoritos.containsKey(emisora.uuid)) {
_favoritos.remove(emisora.uuid);
if (_favoritos.any((e) => e.uuid == emisora.uuid)) {
await eliminar(emisora.uuid);
return false;
}
_favoritos[emisora.uuid] = emisora;
await agregar(emisora);
return true;
}
@override
Future<void> reordenar(String uuid, int nuevoOrden) async {
final oldIndex = _favoritos.indexWhere((e) => e.uuid == uuid);
if (oldIndex == -1 || _favoritos.isEmpty) return;
final targetIndex = nuevoOrden.clamp(0, _favoritos.length - 1);
final moved = _favoritos.removeAt(oldIndex);
_favoritos.insert(targetIndex, moved);
for (var i = 0; i < _favoritos.length; i++) {
_favoritos[i] = _favoritos[i].copyWith(orden: i);
}
}
}
class FakeServicioRadio extends ServicioRadio {
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/tema/pluriwave_theme.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
import 'package:pluriwave/widgets/pluri_icon.dart';
import 'package:pluriwave/widgets/pluri_wave_scaffold.dart';
void main() {
test('PluriWaveTokens.dark mantiene valores base esperados', () {
expect(PluriWaveTokens.dark.deepViolet, const Color(0xFF24123D));
expect(PluriWaveTokens.dark.radiusMd, 16);
expect(PluriWaveTokens.dark.spacingMd, 16);
});
testWidgets('PluriIcon expone semantics label', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: PluriWaveTheme.dark(),
home: const Scaffold(
body: PluriIcon(
glyph: PluriIconGlyph.search,
variant: PluriIconVariant.activeGlow,
semanticLabel: 'Buscar emisoras',
),
),
),
);
expect(find.bySemanticsLabel('Buscar emisoras'), findsOneWidget);
});
testWidgets('PluriGlassSurface y PluriWaveScaffold se instancian', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: PluriWaveTheme.dark(),
home: const PluriWaveScaffold(
body: Center(
child: PluriGlassSurface(
child: Text('ok'),
),
),
),
),
);
expect(find.byType(PluriWaveScaffold), findsOneWidget);
expect(find.byType(PluriGlassSurface), findsOneWidget);
expect(find.text('ok'), findsOneWidget);
});
}
@@ -0,0 +1,48 @@
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';
void main() {
group('VisualizadorAudio lifecycle', () {
testWidgets('ignora eventos de estado después de dispose', (tester) async {
final controller = StreamController<EstadoReproduccion>.broadcast();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: VisualizadorAudio(estadoStream: controller.stream),
),
),
);
await tester.pumpWidget(const SizedBox.shrink());
controller.add(EstadoReproduccion.reproduciendo);
await tester.pump();
expect(tester.takeException(), isNull);
await controller.close();
});
testWidgets('ignora eventos de estado después de dispose en indicador', (tester) async {
final controller = StreamController<EstadoReproduccion>.broadcast();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IndicadorReproduccion(estadoStream: controller.stream),
),
),
);
await tester.pumpWidget(const SizedBox.shrink());
controller.add(EstadoReproduccion.reproduciendo);
await tester.pump();
expect(tester.takeException(), isNull);
await controller.close();
});
});
}