Revision previa al envio a produccion. Cada punto se verifico en el codigo antes de tocarlo; lo que ya estaba bien se dejo como estaba. Ubicacion: se declaraba precision fina sin usarla El unico consumidor de ubicacion pide `LocationAccuracy.low` y se queda solo con el codigo ISO del pais, asi que `ACCESS_FINE_LOCATION` no aportaba nada. Y contradecia la declaracion de Seguridad de los datos ya aprobada en Play, que dice ubicacion APROXIMADA: declarar una cosa y pedir otra es precisamente lo que se penaliza en revision. Verificado que los manifiestos de geolocator_android y geocoding_android no declaran permisos propios, asi que el merge no lo reinyecta y no hace falta `tools:node="remove"`. El plugin construye su peticion en tiempo de ejecucion a partir de lo declarado, de modo que con COARSE pide COARSE. Sin cambio funcional: la deteccion de pais sigue igual. El paywall vendia Android Auto como exclusivo, y ya no lo es La etiqueta era literalmente "Android Auto", a secas. Pero el tier gratuito recibio una carpeta navegable con emisoras reproducibles cuando hubo que cumplir las guias del coche, asi que esa frase dejo de ser cierta. Ahora dice que PRO añade el catalogo completo, favoritos, mis emisoras y musica local, y aclara que gratis tiene las destacadas. Un paywall que promete lo que el tier gratuito ya tiene expone a reclamacion y a que se cite en revision. Microfono: se pide al activar el visualizador, no antes Con una explicacion previa en los 13 idiomas, en vez de aparecer sin contexto. Grabacion: uso privado de verdad, no solo en el aviso La pantalla de grabaciones entregaba el fichero a cualquier aplicacion con `Share.shareXFiles`. La intencion era abrirlo en un reproductor del propio telefono, no redistribuirlo, y una cosa es copia privada y la otra no. Ahora usa el `openFile` que ya existia -- FileProvider + ACTION_VIEW -- y avisa cuando ningun reproductor del dispositivo puede abrirla, en vez de fallar en silencio. Se añade ademas el aviso de uso privado en esa pantalla. `recordingActionShare` la usaban DOS botones con significados distintos: el de grabaciones, que mandaba el audio, y el del reproductor, que comparte el nombre y la url de la emisora. Una clave, dos sentidos, y esa ambiguedad basto para que al leer el codigo pareciera que solo se compartian enlaces. Separadas en `stationActionShare` y `recordingActionOpenIn`. La grabacion sigue siendo PRO. Lo que reduce el riesgo es que la copia no salga del dispositivo, no regalar la funcion: los anuncios tambien son monetizacion. Suite completa: 1587 pasan, 2 omitidos. flutter analyze mantiene los 5 avisos preexistentes.
243 lines
8.9 KiB
Dart
243 lines
8.9 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/estado/estado_alarmas.dart';
|
|
import 'package:pluriwave/estado/estado_busqueda.dart';
|
|
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
|
import 'package:pluriwave/estado/estado_entitlement.dart';
|
|
import 'package:pluriwave/estado/estado_grabacion.dart';
|
|
import 'package:pluriwave/estado/estado_idioma.dart';
|
|
import 'package:pluriwave/estado/estado_radio.dart';
|
|
import 'package:pluriwave/estado/estado_visualizador.dart';
|
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_ajustes.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_buscar.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_inicio.dart';
|
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
|
import 'package:pluriwave/servicios/servicio_anuncios.dart';
|
|
import 'package:pluriwave/widgets/pluri_root_header.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
import '../helpers/fakes_alarmas.dart';
|
|
|
|
/// S1 (Tier 1 visual fidelity): `app.dart`'s `PluriWaveScaffold(appBar:
|
|
/// AppBar(...))` is gone — every root now draws its own [PluriRootHeader]
|
|
/// instead, matching the prototype (no screen in `t4` shows a global
|
|
/// `AppBar`). [PluriRootHeader]'s own title text is asserted via
|
|
/// `find.descendant` throughout — see `pluri_screen_header_retired_test.dart`
|
|
/// for S2's separate assertion that the old glass-hero subtitle is gone.
|
|
///
|
|
/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`):
|
|
/// PluriGlassSurface paints a background over ListTile's ink layer, which
|
|
/// Flutter flags as a warning-level assertion, not a correctness bug.
|
|
void _suppressListTileInkAssertion() {
|
|
final original = FlutterError.onError;
|
|
FlutterError.onError = (details) {
|
|
if (details.exceptionAsString().contains(
|
|
'ListTile background color or ink splashes may be invisible',
|
|
)) {
|
|
return;
|
|
}
|
|
original?.call(details);
|
|
};
|
|
addTearDown(() => FlutterError.onError = original);
|
|
}
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
Future<File> archivoCustomVacio() async => File(
|
|
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
|
);
|
|
|
|
EstadoRadio crearEstadoRadio() => EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
Widget testApp(EstadoRadio estado, Widget body, {EstadoAlarmas? alarmas}) {
|
|
return MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
|
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
|
|
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
|
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
|
|
ChangeNotifierProvider<EstadoIdioma>.value(value: EstadoIdioma()),
|
|
ChangeNotifierProvider<EstadoEntitlement>(
|
|
create: (_) => EstadoEntitlement(prefs: null),
|
|
),
|
|
ChangeNotifierProvider<EstadoVisualizador>(
|
|
create: (_) => EstadoVisualizador(prefs: null),
|
|
),
|
|
Provider<ServicioAnuncios>(
|
|
create: (_) => ServicioAnuncios(esPremium: () => true),
|
|
),
|
|
if (alarmas != null)
|
|
ChangeNotifierProvider<EstadoAlarmas>.value(value: alarmas),
|
|
],
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: body),
|
|
),
|
|
);
|
|
}
|
|
|
|
void setLargeSurface(WidgetTester tester) {
|
|
tester.view.physicalSize = const Size(1440, 3200);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
}
|
|
|
|
Future<void> pumpStable(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
Finder titleInHeader(String title) => find.descendant(
|
|
of: find.byType(PluriRootHeader),
|
|
matching: find.text(title),
|
|
);
|
|
|
|
testWidgets(
|
|
'Escuchar is the documented header exception: a 52px "now listening" '
|
|
'eyebrow instead of a PluriRootHeader title row, and still no AppBar',
|
|
(tester) async {
|
|
// The prototype gives Escuchar a STRUCTURALLY different header from
|
|
// every other root (t4 line 53 vs line 325): 52px rather than 56, an
|
|
// eyebrow at 15px/w800/55% opacity rather than a 23px title, and
|
|
// trailing actions rather than a CTA. This test originally asserted
|
|
// `PluriRootHeader` on all five roots — an over-broad premise, since
|
|
// Escuchar's header was never a title row in the design.
|
|
//
|
|
// What still holds unconditionally, and is what this guards: no
|
|
// AppBar anywhere (systemic item S1), and the sleep-timer action that
|
|
// used to live in the removed global chrome is still reachable here.
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaInicio()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(AppBar), findsNothing);
|
|
expect(
|
|
find.byType(PluriRootHeader),
|
|
findsNothing,
|
|
reason: 'Escuchar uses its own eyebrow header, not the shared one',
|
|
);
|
|
expect(find.text('Now listening'), findsOneWidget);
|
|
expect(
|
|
find.byIcon(Icons.bedtime_outlined),
|
|
findsOneWidget,
|
|
reason: 'the sleep-timer action survived the global AppBar removal',
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Buscar draws its own PluriRootHeader with the search title, no AppBar',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaBuscar()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Search signal'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Favoritos (empty state) draws its own PluriRootHeader, no AppBar',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaFavoritos()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Favorites'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('Alarmas draws its own PluriRootHeader, no AppBar', (
|
|
tester,
|
|
) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
final alarmas = EstadoAlarmas(
|
|
esPremium: () => true,
|
|
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)),
|
|
android: FakePuertoAlarmasAndroid(),
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
addTearDown(alarmas.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
testApp(estado, const PantallaAlarmas(), alarmas: alarmas),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Music wake-up'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'Ajustes draws its own PluriRootHeader, no AppBar, and its bedtime '
|
|
'action opens the sleep-timer sheet',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaAjustes()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Settings'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
|
|
await tester.tap(find.byIcon(Icons.bedtime_outlined));
|
|
await pumpStable(tester);
|
|
|
|
// Not `find.text('Sleep timer')` — Ajustes' own "Sleep timer"
|
|
// FilaAjuste row (`l10n.timerSectionTitle`) coincidentally shares the
|
|
// exact same string as `l10n.sleepTimer`. The description line is
|
|
// unique to the sheet this action opens.
|
|
expect(
|
|
find.text('Smooth radio shutdown with an exact countdown.'),
|
|
findsOneWidget,
|
|
);
|
|
},
|
|
);
|
|
}
|