173 lines
5.8 KiB
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');
|
|
});
|
|
}
|