merge: incorporate main's safearea/auto-order/vacaciones fixes
This commit is contained in:
@@ -607,6 +607,136 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'fix android-auto-orden: el snapshot de favoritos es EXACTAMENTE '
|
||||
'listaFavoritosManual (el orden manual que la pantalla Favoritos '
|
||||
'muestra y reordena), nunca la lista cruda ni un getter reordenado',
|
||||
() async {
|
||||
final favoritosServicio = FakeServicioFavoritos();
|
||||
// Agregados en orden Z, A: si el push usara un getter reordenado
|
||||
// (p. ej. alfabético), 'alfa' iría primero. El orden manual conserva
|
||||
// el orden de inserción/persistencia: Z, A.
|
||||
await favoritosServicio.agregar(
|
||||
emisoraDemo(uuid: 'zulu', nombre: 'Zulu Fav'),
|
||||
);
|
||||
await favoritosServicio.agregar(
|
||||
emisoraDemo(uuid: 'alfa', nombre: 'Alfa Fav'),
|
||||
);
|
||||
final fuenteAuto = _FuenteEmisorasAutoEspia();
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: favoritosServicio,
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
fuenteAuto: fuenteAuto,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(fuenteAuto.ultimoFavoritos?.map((e) => e.uuid).toList(), [
|
||||
'zulu',
|
||||
'alfa',
|
||||
]);
|
||||
expect(fuenteAuto.ultimoFavoritos, equals(estado.listaFavoritosManual));
|
||||
},
|
||||
);
|
||||
|
||||
test('fix android-auto-orden: el snapshot de "Todas" honra el orden '
|
||||
'global (ordenListas) igual que "Tendencias" en el teléfono, no el '
|
||||
'orden crudo de llegada de la API', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'orden_listas_emisoras_v1': 'nombre',
|
||||
});
|
||||
final fuenteAuto = _FuenteEmisorasAutoEspia();
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(
|
||||
populares: [
|
||||
emisoraDemo(uuid: 'zulu-pop', nombre: 'Zulu Pop'),
|
||||
emisoraDemo(uuid: 'alfa-pop', nombre: 'Alfa Pop'),
|
||||
],
|
||||
),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
fuenteAuto: fuenteAuto,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(fuenteAuto.ultimoTodas?.map((e) => e.uuid).toList(), [
|
||||
'alfa-pop',
|
||||
'zulu-pop',
|
||||
]);
|
||||
});
|
||||
|
||||
test('fix android-auto-orden: el snapshot de "Mis emisoras" honra el '
|
||||
'orden global (ordenListas), no el orden crudo del archivo', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'orden_listas_emisoras_v1': 'nombre',
|
||||
});
|
||||
final archivo = await _crearArchivoCustom([
|
||||
emisoraDemo(uuid: 'zulu-custom', nombre: 'Zulu Custom'),
|
||||
emisoraDemo(uuid: 'alfa-custom', nombre: 'Alfa Custom'),
|
||||
]);
|
||||
final fuenteAuto = _FuenteEmisorasAutoEspia();
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
fuenteAuto: fuenteAuto,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(fuenteAuto.ultimoMisEmisoras?.map((e) => e.uuid).toList(), [
|
||||
'alfa-custom',
|
||||
'zulu-custom',
|
||||
]);
|
||||
});
|
||||
|
||||
test('fix android-auto-orden: cambiarOrdenListas re-empuja de inmediato '
|
||||
'los snapshots de "Todas" y "Mis emisoras" con el nuevo orden, sin '
|
||||
'esperar a la próxima recarga completa', () async {
|
||||
final archivo = await _crearArchivoCustom([
|
||||
emisoraDemo(uuid: 'zulu-custom', nombre: 'Zulu Custom'),
|
||||
emisoraDemo(uuid: 'alfa-custom', nombre: 'Alfa Custom'),
|
||||
]);
|
||||
final fuenteAuto = _FuenteEmisorasAutoEspia();
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(
|
||||
populares: [
|
||||
emisoraDemo(uuid: 'zulu-pop', nombre: 'Zulu Pop'),
|
||||
emisoraDemo(uuid: 'alfa-pop', nombre: 'Alfa Pop'),
|
||||
],
|
||||
),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
fuenteAuto: fuenteAuto,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
await estado.cambiarOrdenListas(OrdenEmisoras.nombre);
|
||||
|
||||
expect(fuenteAuto.ultimoTodas?.map((e) => e.uuid).toList(), [
|
||||
'alfa-pop',
|
||||
'zulu-pop',
|
||||
]);
|
||||
expect(fuenteAuto.ultimoMisEmisoras?.map((e) => e.uuid).toList(), [
|
||||
'alfa-custom',
|
||||
'zulu-custom',
|
||||
]);
|
||||
});
|
||||
|
||||
test('reconcilia _emisoraSeleccionada cuando la selección viene desde '
|
||||
'el auto (no via reproducir())', () async {
|
||||
final audio = _AudioControlado();
|
||||
|
||||
@@ -614,4 +614,144 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('fix vacaciones-delete: el editor ofrece una accion de eliminar solo '
|
||||
'al editar un rango existente, reusando _confirmarEliminarRango y '
|
||||
'eliminarRangoVacaciones exactamente como el swipe', () {
|
||||
testWidgets(
|
||||
'creando un rango NUEVO (CTA "Anadir rango"), el editor NO muestra '
|
||||
'una accion de eliminar -- no hay nada que borrar todavia',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.addVacationRangeCta));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(find.text(l10n.newVacationRangeTitle), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('vacation-delete-button')),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('editando un rango EXISTENTE (tap en su tarjeta), el editor SI '
|
||||
'muestra una accion de eliminar junto al boton de guardar', (
|
||||
tester,
|
||||
) async {
|
||||
final estado = await _crearEstado(
|
||||
vacaciones: [
|
||||
RangoVacaciones(
|
||||
id: 'f2',
|
||||
nombre: 'Verano',
|
||||
inicio: _hoyDia.add(const Duration(days: 20)),
|
||||
fin: _hoyDia.add(const Duration(days: 25)),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2')));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
expect(find.text(l10n.editVacationRangeTitle), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('vacation-delete-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'tocar eliminar en el editor pide confirmacion (misma que el swipe); '
|
||||
'cancelar conserva el rango y el editor sigue abierto',
|
||||
(tester) async {
|
||||
final estado = await _crearEstado(
|
||||
vacaciones: [
|
||||
RangoVacaciones(
|
||||
id: 'f2',
|
||||
nombre: 'Verano',
|
||||
inicio: _hoyDia.add(const Duration(days: 20)),
|
||||
fin: _hoyDia.add(const Duration(days: 25)),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2')));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacation-delete-button')));
|
||||
await _pumpEstable(tester);
|
||||
expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text(l10n.cancelAction));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(estado.vacaciones, hasLength(1));
|
||||
expect(find.text(l10n.editVacationRangeTitle), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('tocar eliminar en el editor y confirmar llama a '
|
||||
'eliminarRangoVacaciones y cierra el editor (mismo efecto que el '
|
||||
'swipe, sin pasar por _guardar)', (tester) async {
|
||||
final estado = await _crearEstado(
|
||||
vacaciones: [
|
||||
RangoVacaciones(
|
||||
id: 'f2',
|
||||
nombre: 'Verano',
|
||||
inicio: _hoyDia.add(const Duration(days: 20)),
|
||||
fin: _hoyDia.add(const Duration(days: 25)),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(_buildScreen(estado));
|
||||
await _pumpEstable(tester);
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaVacaciones)),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2')));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacation-delete-button')));
|
||||
await _pumpEstable(tester);
|
||||
expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget);
|
||||
|
||||
// `find.widgetWithText(FilledButton, ...)`, not a bare
|
||||
// `find.text(...)`: the sheet's own OutlinedButton delete action
|
||||
// (same "Eliminar" label) is still in the tree behind the dialog,
|
||||
// so a bare text finder would ambiguously match both.
|
||||
await tester.tap(find.widgetWithText(FilledButton, l10n.deleteAction));
|
||||
await _pumpEstable(tester);
|
||||
|
||||
expect(estado.vacaciones, isEmpty);
|
||||
expect(
|
||||
find.text(l10n.editVacationRangeTitle),
|
||||
findsNothing,
|
||||
reason: 'the sheet must pop, exactly like a successful _guardar',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2741,7 +2741,15 @@ void main() {
|
||||
});
|
||||
|
||||
group('ConstructorArbolAuto.hijos', () {
|
||||
test('limita a 50 items y respeta el orden de ordenarEmisoras', () {
|
||||
test('limita a 50 items y PRESERVA el orden recibido — item 2 '
|
||||
'(android-auto-orden fix): ya no reordena por calidad. Cada llamador '
|
||||
'(Favoritos/Todas/Mis emisoras) empuja el snapshot ya en el orden que '
|
||||
'el teléfono usa (orden manual o el ajuste global ordenListas), así '
|
||||
'que reordenar aquí volvería a descartarlo', () {
|
||||
// bitrate ascendente a propósito: si hijos() todavía reordenara por
|
||||
// calidad (bitrate descendente), el primero sería uuid-59, no
|
||||
// uuid-0 — esta es la diferencia que distingue "preserva el orden"
|
||||
// de "vuelve a ordenar".
|
||||
final emisoras = List.generate(
|
||||
60,
|
||||
(i) => _emisora(uuid: 'uuid-$i', nombre: 'Radio $i', bitrate: i),
|
||||
@@ -2753,11 +2761,8 @@ void main() {
|
||||
);
|
||||
|
||||
expect(hijos, hasLength(50));
|
||||
// ordenarEmisoras por calidad ordena por bitrate descendente: el
|
||||
// primer item debe ser el de mayor bitrate (59) y el ultimo (el #50)
|
||||
// el de bitrate 10 (59..10 son 50 valores).
|
||||
expect(hijos.first.id, 'emisora:uuid-59');
|
||||
expect(hijos.last.id, 'emisora:uuid-10');
|
||||
expect(hijos.first.id, 'emisora:uuid-0');
|
||||
expect(hijos.last.id, 'emisora:uuid-49');
|
||||
});
|
||||
|
||||
test('lista vacía cuando no hay emisoras, sin lanzar', () {
|
||||
@@ -2935,8 +2940,9 @@ void main() {
|
||||
});
|
||||
|
||||
group('ConstructorArbolAuto.hijosGrupo', () {
|
||||
test('filtra por grupoFavoritosId, ordena y capea a 50; id desconocido '
|
||||
'devuelve lista vacía', () {
|
||||
test('filtra por grupoFavoritosId, PRESERVA el orden recibido (ya no '
|
||||
'reordena por calidad — item 2 android-auto-orden fix) y capea a 50; '
|
||||
'id desconocido devuelve lista vacía', () {
|
||||
final favoritos = List.generate(
|
||||
60,
|
||||
(i) => _emisora(
|
||||
@@ -2953,8 +2959,8 @@ void main() {
|
||||
);
|
||||
|
||||
expect(hijos, hasLength(50));
|
||||
expect(hijos.first.id, 'emisora:uuid-59');
|
||||
expect(hijos.last.id, 'emisora:uuid-10');
|
||||
expect(hijos.first.id, 'emisora:uuid-0');
|
||||
expect(hijos.last.id, 'emisora:uuid-49');
|
||||
|
||||
final vacio = ConstructorArbolAuto().hijosGrupo(
|
||||
'grupo:g-inexistente',
|
||||
|
||||
@@ -12,12 +12,23 @@ import 'package:pluriwave/widgets/pluri_root_header.dart';
|
||||
/// sleep-timer action that used to live on `app.dart`'s single global
|
||||
/// `AppBar` stays reachable from every tab.
|
||||
void main() {
|
||||
Widget host(Widget child) {
|
||||
// [topInset] simulates `MediaQuery.paddingOf(context).top` (status bar /
|
||||
// camera cutout) — the same technique already used by
|
||||
// `pantalla_alarma_sonando_scaffold_test.dart`'s `MediaQuery` override via
|
||||
// `MaterialApp.builder`.
|
||||
Widget host(Widget child, {double topInset = 0}) {
|
||||
return MaterialApp(
|
||||
theme: PluriWaveTheme.dark(),
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
builder:
|
||||
(context, app) => MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(padding: EdgeInsets.only(top: topInset)),
|
||||
child: app!,
|
||||
),
|
||||
home: Scaffold(body: child),
|
||||
);
|
||||
}
|
||||
@@ -42,7 +53,13 @@ void main() {
|
||||
host(PluriRootHeader(title: 'Settings', onSleepTimer: () {})),
|
||||
);
|
||||
|
||||
final padding = tester.widget<Padding>(find.byType(Padding).first);
|
||||
// Keyed lookup (not `find.byType(Padding).first`): the top-inset fix
|
||||
// wraps this content padding in an outer `Padding(top: topInset)`, so
|
||||
// `.first` would no longer reliably resolve to the content row's own
|
||||
// padding.
|
||||
final padding = tester.widget<Padding>(
|
||||
find.byKey(const ValueKey('pluri-root-header-content')),
|
||||
);
|
||||
final insets = padding.padding as EdgeInsets;
|
||||
expect(insets.left, PluriLayout.titleHorizontal);
|
||||
expect(insets.right, PluriLayout.rowHorizontal);
|
||||
@@ -97,4 +114,92 @@ void main() {
|
||||
reason: 'actions render before (to the left of) the bedtime button',
|
||||
);
|
||||
});
|
||||
|
||||
group('fix top-inset: total height and content position track '
|
||||
'MediaQuery.paddingOf(context).top, instead of always sitting flush '
|
||||
'at y=0 under the status bar', () {
|
||||
// Vertical text centering inside the 56px content row means the
|
||||
// title's own top-left never sits exactly AT the header's top-left
|
||||
// (even at topInset=0) — so these tests compare each inset's title
|
||||
// position against the topInset=0 BASELINE, isolating exactly the
|
||||
// inset's own contribution instead of asserting a brittle absolute
|
||||
// offset.
|
||||
Future<double> tituloTopPara(WidgetTester tester, double topInset) async {
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
PluriRootHeader(title: 'Settings', onSleepTimer: () {}),
|
||||
topInset: topInset,
|
||||
),
|
||||
);
|
||||
return tester.getTopLeft(find.text('Settings')).dy;
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'topInset=0 (e.g. desktop/no cutout): total height stays the plain '
|
||||
'56px content height',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(PluriRootHeader(title: 'Settings', onSleepTimer: () {})),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byType(PluriRootHeader)).height, 56);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'topInset=24 (typical status bar): total height becomes 56+24=80, '
|
||||
'and the title shifts down by exactly the inset relative to the '
|
||||
'topInset=0 baseline',
|
||||
(tester) async {
|
||||
final base = await tituloTopPara(tester, 0);
|
||||
final conInset = await tituloTopPara(tester, 24);
|
||||
|
||||
expect(tester.getSize(find.byType(PluriRootHeader)).height, 80);
|
||||
expect(conInset - base, 24);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'topInset=44 (taller status bar): total height becomes 56+44=100, '
|
||||
'and the title still shifts down by exactly the inset — never '
|
||||
'clipped',
|
||||
(tester) async {
|
||||
final base = await tituloTopPara(tester, 0);
|
||||
final conInset = await tituloTopPara(tester, 44);
|
||||
|
||||
expect(tester.getSize(find.byType(PluriRootHeader)).height, 100);
|
||||
expect(conInset - base, 44);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'topInset=60 (notch/camera-cutout simulation): total height becomes '
|
||||
'56+60=116, and the title still fully clears the inset — never '
|
||||
'overlapping it',
|
||||
(tester) async {
|
||||
final base = await tituloTopPara(tester, 0);
|
||||
final conInset = await tituloTopPara(tester, 60);
|
||||
|
||||
expect(tester.getSize(find.byType(PluriRootHeader)).height, 116);
|
||||
expect(conInset - base, 60);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'PluriRootHeader.height stays the CONTENT height (56) regardless of '
|
||||
'inset — callers doing total-height math must separately add '
|
||||
'MediaQuery.paddingOf(context).top',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
PluriRootHeader(title: 'Settings', onSleepTimer: () {}),
|
||||
topInset: 44,
|
||||
),
|
||||
);
|
||||
|
||||
expect(PluriRootHeader.height, 56);
|
||||
expect(tester.getSize(find.byType(PluriRootHeader)).height, 100);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user