MO-Fuel-Tax-Back/test/fuel_save_ad_gate_test.dart

173 lines
5.8 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fuel_tax_tracker/app_navigator.dart';
import 'package:fuel_tax_tracker/models/fuel_entry.dart';
import 'package:fuel_tax_tracker/services/app_state.dart';
/// Covers [AppState.maybeShowFuelSaveAd]'s "when" decision — the grace
/// periods and watch-credits that sit in front of the fuel-save ad — by
/// calling it directly against a hand-built [AppState] rather than through
/// a real save, which would need an initialized database.
///
/// With `adsEnabled` false (lib/services/ad_config.dart), the underlying
/// [AdService.maybeShowFuelSaveAd] always resolves to showing the
/// placeholder, so what's under test here is purely the gating above it:
/// if a test sees no placeholder, a gate stopped it.
///
/// These push against the real [navigatorKey], since that's what
/// `showAdPlaceholder` pushes onto — an app that doesn't wire it up would
/// make the placeholder silently do nothing, which is itself one of the
/// failure modes worth pinning down.
void main() {
/// Mirrors a well-used install: past the new-user window and well past
/// the first few saves, with no ad-free purchase.
AppState buildEstablishedUser({int entries = 9}) {
return AppState()
..firstUsedAt = DateTime.now().subtract(const Duration(days: 3))
..adFreeUntil = null
..fuelEntries = List.generate(
entries,
(i) => FuelEntry(
id: 'e$i',
vehicleId: 'v1',
date: DateTime.now(),
gallons: 13.405,
pricePerGallon: 3.899,
totalCost: 52.27,
updatedAt: DateTime.now(),
),
);
}
Future<void> pumpHost(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
home: const Scaffold(body: Text('receipts')),
),
);
}
/// Runs one save's worth of the gate to completion and reports what it
/// decided, running [whileShowing] at the point a placeholder would be
/// on screen.
///
/// The gate's future is deliberately *not* awaited up front: when a
/// placeholder does show, that future only completes once the
/// placeholder pops itself, and it's [WidgetTester.pump] that advances
/// the timer doing the popping — awaiting first deadlocks the two
/// against each other.
Future<bool> runGate(
WidgetTester tester,
AppState appState, {
void Function()? whileShowing,
}) async {
bool? result;
unawaited(appState.maybeShowFuelSaveAd().then((value) => result = value));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
whileShowing?.call();
// Enough for the placeholder's four-second auto-dismiss, harmless
// when nothing came up.
await tester.pump(const Duration(seconds: 4));
await tester.pumpAndSettle();
expect(result, isNotNull, reason: 'the gate should have settled by now');
return result!;
}
testWidgets('an established user gets the ad on save', (tester) async {
await pumpHost(tester);
final appState = buildEstablishedUser();
final shown = await runGate(
tester,
appState,
whileShowing: () => expect(find.text('ADVERTISEMENT'), findsOneWidget),
);
expect(shown, isTrue, reason: 'the caller needs this to hold back the backup reminder');
expect(find.text('receipts'), findsOneWidget, reason: 'placeholder closed itself');
});
testWidgets('cloud backup being unconfigured does not suppress it', (tester) async {
// The regression this is really here for: the gate used to bail out
// whenever backup wasn't set up, which meant a user who never
// configured backup never saw this placement at all.
await pumpHost(tester);
final appState = buildEstablishedUser();
expect(appState.hasCloudBackupConfigured, isFalse, reason: 'no cloudSync wired up');
final shown = await runGate(
tester,
appState,
whileShowing: () => expect(find.text('ADVERTISEMENT'), findsOneWidget),
);
expect(shown, isTrue);
});
testWidgets('no ad during the first five minutes of app usage', (tester) async {
await pumpHost(tester);
final appState = buildEstablishedUser()
..firstUsedAt = DateTime.now().subtract(const Duration(minutes: 2));
final shown = await runGate(
tester,
appState,
whileShowing: () => expect(find.text('ADVERTISEMENT'), findsNothing),
);
expect(shown, isFalse);
});
testWidgets('no ad for the first four saves', (tester) async {
await pumpHost(tester);
final appState = buildEstablishedUser(entries: 4);
final shown = await runGate(
tester,
appState,
whileShowing: () => expect(find.text('ADVERTISEMENT'), findsNothing),
);
expect(shown, isFalse);
});
testWidgets('an active ad-free purchase suppresses it', (tester) async {
await pumpHost(tester);
final appState = buildEstablishedUser()
..adFreeUntil = DateTime.now().toUtc().add(const Duration(days: 300));
final shown = await runGate(
tester,
appState,
whileShowing: () => expect(find.text('ADVERTISEMENT'), findsNothing),
);
expect(shown, isFalse);
});
testWidgets('the credit bought by one ad covers the next two saves', (tester) async {
await pumpHost(tester);
final appState = buildEstablishedUser();
expect(await runGate(tester, appState), isTrue, reason: 'due');
expect(await runGate(tester, appState), isFalse, reason: 'credited save 1');
expect(await runGate(tester, appState), isFalse, reason: 'credited save 2');
final fourth = await runGate(
tester,
appState,
whileShowing: () => expect(find.text('ADVERTISEMENT'), findsOneWidget),
);
expect(fourth, isTrue, reason: 'credit spent — due again');
});
}