Files
pluriwave/test/servicios/servicio_anuncios_test.dart
T
FreeTLab 8c5b29eb3d
Build & Deploy PluriWave / Análisis de código (push) Successful in 27s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 3m19s
feat(anuncios): servir los anuncios reales de AdMob en produccion
La prueba cerrada forzaba los ids de prueba de Google tambien en release,
para que los testers no generaran trafico invalido contra la cuenta de
AdMob. Para la primera version de produccion el interruptor pasa a false:
las builds release sirven el banner y el intersticial reales.

Debug, profile y las pruebas siguen usando siempre los ids de prueba, asi
que desarrollar en un movil propio no toca la cuenta real. El test guardian
fija ahora el valor de produccion, para que nadie vuelva a encenderlo por
descuido y publique banners "Test Ad" que no ingresan nada.
2026-10-03 08:49:22 +02:00

218 lines
7.7 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/servicios/servicio_anuncios.dart';
/// Ad-display spec: session-scoped interstitial frequency cap (max 2 per
/// process lifetime, >=3 min apart), suppressed entirely for premium, and
/// never shown when the caller reports the alarm-cap message took
/// precedence for this same tap.
void main() {
ServicioAnuncios construir({
required DateTime Function() ahora,
required bool premium,
Future<bool> Function()? mostrarInterstitialImpl,
Duration? timeoutIntentoInterstitial,
}) => ServicioAnuncios(
ahora: ahora,
esPremium: () => premium,
mostrarInterstitialImpl: mostrarInterstitialImpl ?? (() async => true),
timeoutIntentoInterstitial: timeoutIntentoInterstitial,
);
group('intentarInterstitial — cap de frecuencia', () {
test('primeros 2 intentos en la sesion se muestran', () async {
var ahora = DateTime(2026, 1, 1, 10, 0);
final servicio = construir(ahora: () => ahora, premium: false);
expect(await servicio.intentarInterstitial(), isTrue);
ahora = ahora.add(const Duration(minutes: 5));
expect(await servicio.intentarInterstitial(), isTrue);
});
test('un 3er intento en la misma sesion no se muestra (cap 2)', () async {
var ahora = DateTime(2026, 1, 1, 10, 0);
final servicio = construir(ahora: () => ahora, premium: false);
await servicio.intentarInterstitial();
ahora = ahora.add(const Duration(minutes: 5));
await servicio.intentarInterstitial();
ahora = ahora.add(const Duration(minutes: 5));
expect(await servicio.intentarInterstitial(), isFalse);
});
test('menos de 3 minutos desde el ultimo: no se muestra', () async {
var ahora = DateTime(2026, 1, 1, 10, 0);
final servicio = construir(ahora: () => ahora, premium: false);
await servicio.intentarInterstitial();
ahora = ahora.add(const Duration(minutes: 1));
expect(await servicio.intentarInterstitial(), isFalse);
});
test('exactamente 3 minutos despues si se muestra', () async {
var ahora = DateTime(2026, 1, 1, 10, 0);
final servicio = construir(ahora: () => ahora, premium: false);
await servicio.intentarInterstitial();
ahora = ahora.add(const Duration(minutes: 3));
expect(await servicio.intentarInterstitial(), isTrue);
});
test('premium: nunca muestra interstitial', () async {
final servicio = construir(
ahora: () => DateTime(2026, 1, 1, 10, 0),
premium: true,
);
expect(await servicio.intentarInterstitial(), isFalse);
});
test(
'el conteo/temporizador solo avanza si el ad realmente se muestra',
() async {
final ahora = DateTime(2026, 1, 1, 10, 0);
final servicio = construir(
ahora: () => ahora,
premium: false,
mostrarInterstitialImpl: () async => false,
);
final mostrado = await servicio.intentarInterstitial();
expect(mostrado, isFalse);
// Un fallo de carga (mostrarInterstitialImpl -> false) no debe
// consumir el cupo de la sesion.
expect(await servicio.intentarInterstitial(), isFalse);
},
);
});
group('intentarInterstitial — fallo al mostrar no consume el cupo', () {
test('la implementación real de AdMob distingue presentación real de '
'fallo de renderizado (FIX 6, code review): '
'onAdShowedFullScreenContent debe estar instrumentado y el resultado '
'de show() no puede ser un true incondicional', () {
final source =
File('lib/servicios/servicio_anuncios.dart').readAsStringSync();
expect(
source.contains('onAdShowedFullScreenContent'),
isTrue,
reason:
'debe registrar si el anuncio realmente llegó a presentarse '
'(onAdShowedFullScreenContent) para no devolver true tras un '
'onAdFailedToShowFullScreenContent',
);
expect(
source.contains('await cierreCompleter.future;\n return true;'),
isFalse,
reason:
'el resultado de _mostrarInterstitialAdMob ya no puede ser un '
'true incondicional tras el cierre -- debe reflejar si el '
'anuncio realmente se presentó',
);
});
});
group('intentarInterstitial — timeout acotado (FIX 2, code review)', () {
test('una implementación que nunca completa resuelve false dentro del '
'timeout inyectado y no consume el cupo', () async {
final ahora = DateTime(2026, 1, 1, 10, 0);
final nuncaCompleta = Completer<bool>();
addTearDown(() {
if (!nuncaCompleta.isCompleted) nuncaCompleta.complete(false);
});
final servicio = construir(
ahora: () => ahora,
premium: false,
mostrarInterstitialImpl: () => nuncaCompleta.future,
timeoutIntentoInterstitial: const Duration(milliseconds: 20),
);
final resultado = await servicio.intentarInterstitial().timeout(
const Duration(seconds: 2),
);
expect(resultado, isFalse);
// El cupo no se consumió: un intento posterior con una
// implementación que SÍ resuelve sigue mostrando el anuncio.
final servicioReal = construir(ahora: () => ahora, premium: false);
expect(await servicioReal.intentarInterstitial(), isTrue);
});
test('una implementación lenta pero que SÍ completa dentro del timeout '
'sigue resolviendo con su resultado real', () async {
final ahora = DateTime(2026, 1, 1, 10, 0);
final servicio = construir(
ahora: () => ahora,
premium: false,
mostrarInterstitialImpl: () async {
await Future<void>.delayed(const Duration(milliseconds: 5));
return true;
},
timeoutIntentoInterstitial: const Duration(milliseconds: 200),
);
expect(await servicio.intentarInterstitial(), isTrue);
});
});
group('usarAnunciosDePruebaEnRelease — interruptor de fase de pruebas', () {
test('en produccion el interruptor esta apagado: las builds release '
'sirven los anuncios reales', () {
// Flipped for the first production release (1.3.3+166). Re-enabling it
// would silently ship Google's "Test Ad" banners and earn nothing.
expect(usarAnunciosDePruebaEnRelease, isFalse);
});
test('fuera de release (debug, profile y pruebas) se usan siempre los ids '
'oficiales de prueba de Google', () {
expect(bannerAdUnitId, equals('ca-app-pub-3940256099942544/6300978111'));
expect(
interstitialAdUnitId,
equals('ca-app-pub-3940256099942544/1033173712'),
);
});
test('los ids reales siguen presentes como constantes en el archivo (no se '
'pueden perder en un futuro edit)', () {
final source =
File('lib/servicios/servicio_anuncios.dart').readAsStringSync();
expect(
source.contains("'ca-app-pub-6038935671414339/5658618378'"),
isTrue,
reason: 'el id real del banner debe seguir presente en el archivo',
);
expect(
source.contains("'ca-app-pub-6038935671414339/4189478248'"),
isTrue,
reason:
'el id real del interstitial debe seguir presente en el '
'archivo',
);
});
});
group('debeMostrarBanner', () {
test('free: true', () {
final servicio = construir(
ahora: () => DateTime(2026, 1, 1),
premium: false,
);
expect(servicio.debeMostrarBanner, isTrue);
});
test('premium: false', () {
final servicio = construir(
ahora: () => DateTime(2026, 1, 1),
premium: true,
);
expect(servicio.debeMostrarBanner, isFalse);
});
});
}