diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart index ec3feb6..d8c4e89 100644 --- a/lib/pantallas/pantalla_favoritos.dart +++ b/lib/pantallas/pantalla_favoritos.dart @@ -58,23 +58,35 @@ class _PantallaFavoritosState extends State { /// global position [EstadoRadio.reordenarFavorito] expects, so a drag /// while a group chip is active still produces a coherent global order /// (other groups' relative order is left untouched). + /// + /// [newIndex] arrives in `ReorderableListView.onReorder`'s pre-removal + /// coordinate space: dragging downwards reports the slot the row would + /// occupy while it is still in the list. The logic below indexes into the + /// list AFTER the row is removed, so shift by one in that direction first. void _onReorder( List filtrados, List favoritos, int oldIndex, int newIndex, ) { + if (newIndex > oldIndex) newIndex -= 1; final movido = filtrados[oldIndex]; final restantes = List.from(filtrados)..removeAt(oldIndex); + // `ServicioFavoritos.reordenar` removes the station first and THEN + // inserts at the index it is given, so the target index must be + // expressed in the global list WITHOUT the moved station. Locating the + // neighbour in the untrimmed list instead drifts by one whenever the + // moved station sits before it. + final globalSinMovido = + favoritos.where((e) => e.uuid != movido.uuid).toList(); final int nuevoIndiceGlobal; if (restantes.isEmpty) { - nuevoIndiceGlobal = favoritos.length - 1; + nuevoIndiceGlobal = globalSinMovido.length; } else if (newIndex >= restantes.length) { - nuevoIndiceGlobal = favoritos.indexWhere( - (e) => e.uuid == restantes.last.uuid, - ); + nuevoIndiceGlobal = + globalSinMovido.indexWhere((e) => e.uuid == restantes.last.uuid) + 1; } else { - nuevoIndiceGlobal = favoritos.indexWhere( + nuevoIndiceGlobal = globalSinMovido.indexWhere( (e) => e.uuid == restantes[newIndex].uuid, ); } @@ -217,7 +229,7 @@ class _PantallaFavoritosState extends State { onTap: _abrirFormularioEmisoraPersonalizada, ), ), - onReorderItem: + onReorder: (oldIndex, newIndex) => _onReorder(filtrados, favoritos, oldIndex, newIndex), children: [ diff --git a/test/l10n/arb_anti_copy_test.dart b/test/l10n/arb_anti_copy_test.dart index 17aefb4..9cc461f 100644 --- a/test/l10n/arb_anti_copy_test.dart +++ b/test/l10n/arb_anti_copy_test.dart @@ -44,8 +44,8 @@ void main() { for (final locale in _auditedLocales) { final arb = readArb(locale); for (final key in realKeys(arb)) { - if (!es.containsKey(key)) - continue; // arb_parity_test's job, not this one's + // Missing keys are arb_parity_test's job, not this one's. + if (!es.containsKey(key)) continue; if (arb[key] == es[key] && !identicalValueAllowlist.contains((locale, key))) { unlisted.add('$locale/$key = "${arb[key]}"'); diff --git a/test/pantallas/pantalla_favoritos_test.dart b/test/pantallas/pantalla_favoritos_test.dart index 86d8d8b..30aa562 100644 --- a/test/pantallas/pantalla_favoritos_test.dart +++ b/test/pantallas/pantalla_favoritos_test.dart @@ -226,10 +226,13 @@ void main() { find.byType(ReorderableListView), ); // Drag the 3rd row (index 2, "Station C") to the 1st position (index - // 0) — exercised via the real onReorderItem callback the widget wires - // up. onReorderItem (not the deprecated onReorder) already adjusts - // newIndex for the removed item, so no manual index math here. - lista.onReorderItem!(2, 0); + // 0), exercised via the real onReorder callback the widget wires up. + // `onReorder` is the API present across Flutter versions (the newer + // `onReorderItem` does not exist on the CI SDK), so it reports + // newIndex in the PRE-removal coordinate space; `_onReorder` + // compensates internally. Moving upwards needs no shift, which is why + // (2, 0) maps straight through. + lista.onReorder!(2, 0); await pumpStable(tester); expect(estado.listaFavoritosManual.map((e) => e.uuid).toList(), [ @@ -247,6 +250,40 @@ void main() { ]); }); + testWidgets('dragging the 1st item downwards lands it in the right slot', ( + tester, + ) async { + // Guards the pre-removal index compensation in `_onReorder`. Downward + // drags are the ONLY direction `ReorderableListView.onReorder` reports + // in the pre-removal coordinate space, so an off-by-one here would slip + // past the upward-drag test above entirely. + setLargeSurface(tester); + _suppressListTileInkAssertion(); + final estado = await crearEstadoConFavoritos(); + addTearDown(estado.dispose); + + await tester.pumpWidget(buildScreen(estado)); + await pumpStable(tester); + + final lista = tester.widget( + find.byType(ReorderableListView), + ); + // Move "Station A" (index 0) into the MIDDLE slot. onReorder reports + // newIndex == 2 in the pre-removal space; `_onReorder` shifts it to 1. + // This specific case is what makes the test meaningful: dropping at the + // very end (0, 3) yields the same answer with or without the shift, + // because both land in the `newIndex >= restantes.length` branch. Only a + // mid-list drop separates the two. + lista.onReorder!(0, 2); + await pumpStable(tester); + + expect(estado.listaFavoritosManual.map((e) => e.uuid).toList(), [ + 'b', + 'a', + 'c', + ]); + }); + testWidgets( 'swap_vert sort action applies OrdenEmisoras.nombre and re-renders ' 'alphabetically',