Fixes 9 of 10 review findings (10th requires a manual Play Console step, no code change): 1. app.dart/banner_anuncio_superior.dart: move the top SafeArea inside BannerAnuncioSuperior so it only reserves status-bar height when an ad actually renders, restoring edge-to-edge layout for premium and free-unloaded users. 2. servicio_anuncios.dart: bound every interstitial await (load, presentation, and the injected implementation itself) with injectable timeouts so a callback that never fires can no longer hang a caller. 3. estado_entitlement.dart/hoja_premium.dart: expose a typed resultadoUsuario signal for purchase/restore failures and restore-found-nothing, with dedicated localized messages (compraError, restauracionSinCompras) across all 13 locales -- never the raw developer/exception string. 4. main.dart/servicio_consentimiento.dart: add a GDPR/UMP consent flow (ConsentInformation/ConsentForm) that gates Mobile Ads SDK init on canRequestAds(); premium users never see a consent form; failures degrade to no ads instead of crashing or blocking startup. 6. servicio_anuncios.dart: track real ad presentation (onAdShowedFullScreenContent) so a failed-to-show interstitial no longer consumes a session cap slot. 7. banner_anuncio_superior.dart: add an explicit load-attempted guard so repeated didChangeDependencies (e.g. entitlement notifyListeners during a purchase) can only ever trigger one banner load attempt. 8. servicio_anuncios.dart: make esPremium a required constructor parameter, matching the hardened contract already applied to EstadoAlarmas/EstadoGrabacion/EstadoRadio. 9. hoja_premium.dart: add a dedicated premiumActivo localized string instead of reusing the equalizer's equalizerActive translation, across all 13 locales. All fixes implemented RED-first (failing test before production code). Full suite: 1261 passed, 2 pre-existing skips, 0 failures. flutter analyze: 5 pre-existing issues only, 0 new. [version set]
139 lines
5.0 KiB
Dart
139 lines
5.0 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/app.dart';
|
|
import 'package:pluriwave/estado/estado_entitlement.dart';
|
|
import 'package:pluriwave/servicios/servicio_anuncios.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// S1 (Tier 1 visual fidelity): the prototype (`t4`) never draws a global
|
|
/// `AppBar` — every root owns its own 56px title row instead (see
|
|
/// `PluriRootHeader` and `root_header_wiring_test.dart`). `app.dart` used to
|
|
/// wrap every tab in `PluriWaveScaffold(appBar: AppBar(...))`; this is a
|
|
/// fast source-level regression guard for that removal, since the widget
|
|
/// under it (`_PaginaPrincipal`) is library-private and constructs real
|
|
/// platform-backed services, so it cannot be safely widget-tested here.
|
|
void main() {
|
|
test('app.dart no longer constructs a global Material AppBar', () {
|
|
final source = File('lib/app.dart').readAsStringSync();
|
|
expect(
|
|
source.contains('AppBar('),
|
|
isFalse,
|
|
reason:
|
|
'the prototype has no global app bar — each root draws its own '
|
|
'PluriRootHeader inside its content instead',
|
|
);
|
|
});
|
|
|
|
test('the first-launch sequence runs the welcome screen, then the tutorial '
|
|
'carousel, then the recurring what-is-new dialog, in that order', () {
|
|
// `_PaginaPrincipal` is library-private and its
|
|
// `_mostrarFlujoPrimerLanzamiento` constructs real platform-backed
|
|
// services, so it cannot be safely widget-tested here (same
|
|
// constraint as the AppBar guard above). This is a fast source-level
|
|
// ordering guard instead: PantallaBienvenida.mostrarSiProcede must
|
|
// run before PantallaTutorialAyuda.mostrarSiProcede, which must run
|
|
// before the unrelated, pre-existing _mostrarOnboardingInicial() call
|
|
// -- so the tutorial shows once on every launch sequence (fresh
|
|
// installs AND existing installs upgrading to this version) without
|
|
// ever racing the welcome screen or the what's-new dialog.
|
|
final source = File('lib/app.dart').readAsStringSync();
|
|
|
|
final indiceBienvenida = source.indexOf(
|
|
'PantallaBienvenida.mostrarSiProcede',
|
|
);
|
|
final indiceTutorial = source.indexOf(
|
|
'PantallaTutorialAyuda.mostrarSiProcede',
|
|
);
|
|
final indiceOnboarding = source.indexOf('_mostrarOnboardingInicial()');
|
|
|
|
expect(
|
|
indiceBienvenida,
|
|
greaterThanOrEqualTo(0),
|
|
reason: 'PantallaBienvenida.mostrarSiProcede must still be called',
|
|
);
|
|
expect(
|
|
indiceTutorial,
|
|
greaterThanOrEqualTo(0),
|
|
reason: 'PantallaTutorialAyuda.mostrarSiProcede must be wired in',
|
|
);
|
|
expect(
|
|
indiceOnboarding,
|
|
greaterThanOrEqualTo(0),
|
|
reason: '_mostrarOnboardingInicial() must still be called',
|
|
);
|
|
expect(
|
|
indiceBienvenida,
|
|
lessThan(indiceTutorial),
|
|
reason: 'the welcome screen must run before the tutorial carousel',
|
|
);
|
|
expect(
|
|
indiceTutorial,
|
|
lessThan(indiceOnboarding),
|
|
reason: 'the tutorial carousel must run before the what-is-new dialog',
|
|
);
|
|
});
|
|
|
|
group(
|
|
'construirCuerpoPrincipal — banner y la status bar (FIX 1, code review)',
|
|
() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
Future<void> bombear(WidgetTester tester, {required bool premium}) async {
|
|
await tester.pumpWidget(
|
|
MediaQuery(
|
|
data: const MediaQueryData(padding: EdgeInsets.only(top: 44)),
|
|
child: MaterialApp(
|
|
home: MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<EstadoEntitlement>(
|
|
create: (_) => EstadoEntitlement(prefs: null),
|
|
),
|
|
Provider<ServicioAnuncios>(
|
|
create: (_) => ServicioAnuncios(esPremium: () => premium),
|
|
),
|
|
],
|
|
child: Scaffold(
|
|
body: construirCuerpoPrincipal(
|
|
contenido: const Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Text('contenido'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
}
|
|
|
|
testWidgets(
|
|
'usuario premium: el contenido arranca en y=0 -- edge-to-edge, sin '
|
|
'franja en blanco reservada para la status bar',
|
|
(tester) async {
|
|
await bombear(tester, premium: true);
|
|
|
|
expect(tester.getTopLeft(find.text('contenido')).dy, 0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'usuario free con el banner aún sin cargar: el contenido arranca '
|
|
'igualmente en y=0 -- misma posición edge-to-edge que antes del '
|
|
'cambio, no una franja reservada de 44px hasta que el ad cargue',
|
|
(tester) async {
|
|
await bombear(tester, premium: false);
|
|
|
|
expect(tester.getTopLeft(find.text('contenido')).dy, 0);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|