Files
pluriwave/test/servicios/servicio_anuncios_test.dart
T
FreeTLab aa0b242374 feat(iap): add freemium unlock via one-time in-app purchase
Adds a permanent, non-consumable premium unlock (EstadoEntitlement +
PuertoCompras/ServicioComprasPlayBilling) that removes ads and unlocks
alarm vacations, alarms past a 5-alarm free cap, recording start, and
full Android Auto browsing. The phone equalizer stays free for everyone.

- Entitlement is prefs-backed (compra_premium_v1), fail-open, and
  resolvable headlessly via esPremiumPersistido() for the Android Auto
  audio handler, which registers before runApp.
- Android Auto reduced mode keeps the real root folder labels for free
  users; browsing into any of them (and playFromMediaId/playFromSearch/
  skipToNext/skipToPrevious) is blocked at the getChildren/servicio_audio
  choke points, with a locked "Función Premium" item as the backstop.
  Current-station play/pause/stop stays untouched. A free -> premium
  transition actively invalidates the head unit's cached browse tree.
- Ads (top banner + capped interstitial before adding a station or an
  alarm) are gated behind entitlement via ServicioAnuncios, using
  official Google test ad unit IDs pending AdMob provisioning.
- Alarm cap UX shows an explanatory message with a secondary unlock
  action rather than a bare paywall jump; existing data is grandfathered.
- 4 new localization keys translated across all 13 supported locales.

Co-located tests use strict TDD (RED test before implementation) for
every new pure-logic unit; full existing suite passes unchanged.
2026-08-10 20:37:07 +02:00

108 lines
3.5 KiB
Dart

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,
}) => ServicioAnuncios(
ahora: ahora,
esPremium: () => premium,
mostrarInterstitialImpl: mostrarInterstitialImpl ?? (() async => true),
);
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('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);
});
});
}