Temporary setup of alternative to advertisements
This commit is contained in:
parent
ad2d932842
commit
57a9793a40
14 changed files with 653 additions and 56 deletions
173
test/fuel_save_ad_gate_test.dart
Normal file
173
test/fuel_save_ad_gate_test.dart
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
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');
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue