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.
313 lines
11 KiB
Dart
313 lines
11 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/widgets/pluri_push_scaffold.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
import '../helpers/fakes_alarmas.dart';
|
|
|
|
/// Design ADR-2 enforcement test: chrome is decided by route topology, not a
|
|
/// parameter. Mounting each of the 5 root screens bare (no ancestor
|
|
/// PluriWaveScaffold/Scaffold) must render zero Scaffolds — "no tab bar on
|
|
/// second-level screens" becomes a structural fact, not a convention. A
|
|
/// PluriPushScaffold, by contrast, is the ONE place a second-level screen
|
|
/// gets a Scaffold, and it must render exactly one 56px AppBar with a back
|
|
/// affordance.
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
Widget testApp(Widget home) {
|
|
return MaterialApp(
|
|
locale: const Locale('es'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
// No Scaffold ancestor here on purpose — a bare Material is enough
|
|
// for InkWell/Card without secretly providing the very Scaffold this
|
|
// test asserts is absent.
|
|
home: Material(child: home),
|
|
);
|
|
}
|
|
|
|
Future<File> archivoCustomVacio() async => File(
|
|
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
|
);
|
|
|
|
// Existing project convention (see pantalla_inicio_test.dart et al.): the
|
|
// default 800x600 test surface clips these content-heavy root screens.
|
|
void setLargeSurface(WidgetTester tester) {
|
|
tester.view.physicalSize = const Size(1440, 3200);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
group('The 5 root screens build zero Scaffold when mounted bare', () {
|
|
testWidgets('PantallaInicio', (tester) async {
|
|
setLargeSurface(tester);
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
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),
|
|
],
|
|
child: testApp(const PantallaInicio()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaBuscar', (tester) async {
|
|
setLargeSurface(tester);
|
|
// WU6 correction: PantallaBuscar now also owns the discovery landing
|
|
// state relocated from PantallaInicio (task 6.5), which reads
|
|
// EstadoRadio directly (near-you/trending/browse-grid selectors) —
|
|
// a bare EstadoBusqueda provider is no longer sufficient.
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
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),
|
|
],
|
|
child: testApp(const PantallaBuscar()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaFavoritos', (tester) async {
|
|
setLargeSurface(tester);
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<EstadoRadio>.value(
|
|
value: estado,
|
|
child: testApp(const PantallaFavoritos()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaAlarmas', (tester) async {
|
|
setLargeSurface(tester);
|
|
final android = FakePuertoAlarmasAndroid();
|
|
final estadoAlarmas = EstadoAlarmas(
|
|
esPremium: () => true,
|
|
servicio: ServicioAlarmas(reloj: DateTime.now),
|
|
android: android,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estadoAlarmas.dispose);
|
|
addTearDown(android.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<EstadoAlarmas>.value(
|
|
value: estadoAlarmas,
|
|
child: testApp(const PantallaAlarmas()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaAjustes', (tester) async {
|
|
setLargeSurface(tester);
|
|
suppressListTileInkAssertion();
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
final estadoIdioma = EstadoIdioma();
|
|
addTearDown(estadoIdioma.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
|
ListenableProvider<EstadoEcualizador>.value(
|
|
value: estado.ecualizador,
|
|
),
|
|
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
|
ChangeNotifierProvider<EstadoIdioma>.value(value: estadoIdioma),
|
|
ChangeNotifierProvider<EstadoEntitlement>(
|
|
create: (_) => EstadoEntitlement(prefs: null),
|
|
),
|
|
ChangeNotifierProvider<EstadoVisualizador>(
|
|
create: (_) => EstadoVisualizador(prefs: null),
|
|
),
|
|
],
|
|
child: testApp(const PantallaAjustes()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('PluriPushScaffold', () {
|
|
testWidgets('renders exactly one 56px AppBar with a back affordance', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Navigator(
|
|
onGenerateRoute:
|
|
(settings) => MaterialPageRoute<void>(
|
|
builder:
|
|
(_) => const PluriPushScaffold(
|
|
title: 'Detalle',
|
|
body: SizedBox.shrink(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Scaffold), findsOneWidget);
|
|
expect(find.byType(AppBar), findsOneWidget);
|
|
final appBar = tester.widget<AppBar>(find.byType(AppBar));
|
|
expect(appBar.toolbarHeight, PluriPushScaffold.headerHeight);
|
|
expect(PluriPushScaffold.headerHeight, 56);
|
|
expect(find.byIcon(Icons.arrow_back_rounded), findsOneWidget);
|
|
expect(find.text('Detalle'), findsOneWidget);
|
|
});
|
|
|
|
// Design ADR-2's load-bearing contract: PluriPushScaffold has NO
|
|
// bottomNavigationBar parameter. This is a type-system guarantee, not a
|
|
// runtime one — passing `bottomNavigationBar: ...` below would simply
|
|
// fail to compile. There is nothing to assert dynamically; the absence
|
|
// of that parameter IS the enforcement.
|
|
|
|
testWidgets('push navigates via Navigator.push (route depth increases)', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Builder(
|
|
builder:
|
|
(context) => Material(
|
|
child: Center(
|
|
child: ElevatedButton(
|
|
onPressed:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PluriPushScaffold(
|
|
title: 'Empujada',
|
|
body: SizedBox.shrink(),
|
|
),
|
|
),
|
|
child: const Text('abrir'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsNothing);
|
|
await tester.tap(find.text('abrir'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('Empujada'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|