test(store): generate IAP review screenshot for App Store (remove_ads)
Headless render of the Settings screen showing the Remove Ads purchase point + price + Restore Purchases, for the App Store IAP review-screenshot requirement. Forces a desktop target platform so the real IapService can be constructed without the store plugin making async channel calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
@@ -0,0 +1,128 @@
|
||||
// Headless generator for the App Store IAP review screenshot: renders the
|
||||
// Settings screen (where "광고 제거 / Remove Ads" is purchased) to a PNG.
|
||||
//
|
||||
// flutter test test/tool/generate_iap_review_screenshot_test.dart
|
||||
//
|
||||
// Output: docs/store/iap_review_remove_ads.png
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:block_seasons/l10n/gen/app_localizations.dart';
|
||||
import 'package:block_seasons/services/iap_service.dart';
|
||||
import 'package:block_seasons/state/providers.dart';
|
||||
import 'package:block_seasons/ui/screens/settings_screen.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// A stand-in IAP service that reports the product as available with a price,
|
||||
// so the Settings row renders exactly as a shopper sees it. initialize() is
|
||||
// never called, so no real store query happens.
|
||||
class _FakeIap extends IapService {
|
||||
_FakeIap() : super(onEntitlementGranted: _noop);
|
||||
static Future<void> _noop() async {}
|
||||
|
||||
@override
|
||||
bool get available => true;
|
||||
|
||||
@override
|
||||
ProductDetails get product => ProductDetails(
|
||||
id: 'remove_ads',
|
||||
title: 'Remove Ads',
|
||||
description: 'Remove banner and interstitial ads',
|
||||
price: r'$0.99',
|
||||
rawPrice: 0.99,
|
||||
currencyCode: 'USD',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadFont() async {
|
||||
final bytes =
|
||||
File('/System/Library/Fonts/Supplemental/Arial.ttf').readAsBytesSync();
|
||||
final loader = FontLoader('Arial')
|
||||
..addFont(Future.value(ByteData.view(bytes.buffer)));
|
||||
await loader.load();
|
||||
}
|
||||
|
||||
Widget _wrap(ProviderContainer c, Widget screen, GlobalKey key) =>
|
||||
UncontrolledProviderScope(
|
||||
container: c,
|
||||
child: RepaintBoundary(
|
||||
key: key,
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Arial',
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF5B7FFF),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: screen,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('generate IAP review screenshot', (tester) async {
|
||||
// in_app_purchase only registers a native platform for android/iOS/macOS;
|
||||
// on a desktop target it registers none, so constructing the real
|
||||
// IapService never makes a store channel call (which would throw async in
|
||||
// the headless binding). The fake overrides available/product anyway.
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
|
||||
await _loadFont();
|
||||
|
||||
const dpr = 3.0;
|
||||
tester.view.devicePixelRatio = dpr;
|
||||
tester.view.physicalSize = const Size(1242, 2688); // 6.5" — accepted size
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = SaveRepository(await SharedPreferences.getInstance());
|
||||
|
||||
final key = GlobalKey();
|
||||
final c = ProviderContainer(overrides: [
|
||||
saveRepositoryProvider.overrideWithValue(repo),
|
||||
iapServiceProvider.overrideWithValue(_FakeIap()),
|
||||
]);
|
||||
await tester.pumpWidget(_wrap(c, const SettingsScreen(), key));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 450));
|
||||
|
||||
var ro = key.currentContext!.findRenderObject()!;
|
||||
while (!ro.isRepaintBoundary) {
|
||||
ro = ro.parent!;
|
||||
}
|
||||
final layer = ro.debugLayer! as OffsetLayer;
|
||||
final bounds = ro.paintBounds;
|
||||
final bytes = await tester.runAsync(() async {
|
||||
final image = await layer.toImage(bounds, pixelRatio: dpr);
|
||||
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
image.dispose();
|
||||
return data;
|
||||
});
|
||||
File('docs/store/iap_review_remove_ads.png')
|
||||
..parent.createSync(recursive: true)
|
||||
..writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
c.dispose();
|
||||
|
||||
// Reset before the body ends so flutter_test's foundation-var invariant
|
||||
// check (which runs before addTearDown) passes.
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
|
||||
expect(
|
||||
File('docs/store/iap_review_remove_ads.png').existsSync(), isTrue);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user