import 'dart:io'; import 'dart:ui' as ui; 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:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/theme_settings.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; import 'package:spotiflac_android/screens/upgrade_intro_screen.dart'; import 'package:spotiflac_android/services/upgrade_intro_service.dart'; import 'package:spotiflac_android/theme/app_theme.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async { await (FontLoader( 'MaterialIcons', )..addFont(rootBundle.load('fonts/MaterialIcons-Regular.otf'))).load(); await (FontLoader( 'Google Sans Flex', )..addFont(rootBundle.load('assets/fonts/GoogleSansFlex.ttf'))).load(); await (FontLoader( 'Inter', )..addFont(rootBundle.load('assets/fonts/InterVariable.ttf'))).load(); await (FontLoader('packages/cupertino_icons/CupertinoIcons')..addFont( rootBundle.load('packages/cupertino_icons/assets/CupertinoIcons.ttf'), )) .load(); }); test( 'upgrade tour is acknowledged separately from settings migrations', () async { SharedPreferences.setMockInitialValues({ 'settings_migration_version': 11, }); var prefs = await SharedPreferences.getInstance(); bool pending(String version, {bool existing = true}) => UpgradeIntroService.shouldShow( prefs, version: version, existingInstallation: existing, ); expect(pending('5.0.0'), isTrue); expect(pending('5.0.1'), isTrue); expect(pending('4.9.0'), isFalse); expect(pending('6.0.0'), isFalse); expect(pending('5.0.0', existing: false), isFalse); await UpgradeIntroService.markSeen(prefs); prefs = await SharedPreferences.getInstance(); expect(pending('5.0.0'), isFalse); expect(prefs.getInt('settings_migration_version'), 11); }, ); final capture = GlobalKey(); Future openTour( WidgetTester tester, { Size size = const Size(390, 844), double textScale = 1, Brightness brightness = Brightness.light, AppThemeStyle style = AppThemeStyle.mornye, }) async { SharedPreferences.setMockInitialValues({kThemeStyleKey: style.name}); tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.pumpWidget( ProviderScope( overrides: [ initialThemeSettingsProvider.overrideWithValue( ThemeSettings(style: style), ), ], child: Consumer( builder: (context, ref, _) { final selectedStyle = ref.watch( themeProvider.select((value) => value.style), ); return MaterialApp( theme: selectedStyle == AppThemeStyle.mornye ? MornyeTheme.build(brightness) : brightness == Brightness.dark ? AppTheme.dark() : AppTheme.light(), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, builder: (context, child) => MediaQuery( data: MediaQuery.of( context, ).copyWith(textScaler: TextScaler.linear(textScale)), child: RepaintBoundary(key: capture, child: child), ), home: Scaffold( body: Builder( builder: (context) => TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => const UpgradeIntroScreen(), ), ), child: const Text('Open tour'), ), ), ), ); }, ), ), ); await tester.tap(find.text('Open tour')); await tester.pumpAndSettle(); } Future capturePage(WidgetTester tester, String name) async { if (!const bool.fromEnvironment('CAPTURE_UPGRADE_INTRO')) return; await tester.runAsync(() async { final boundary = capture.currentContext!.findRenderObject()! as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 2); final bytes = await image.toByteData(format: ui.ImageByteFormat.png); await File( '/tmp/spotiflac-intro-$name.png', ).writeAsBytes(bytes!.buffer.asUint8List()); image.dispose(); }); } for (final brightness in Brightness.values) { testWidgets('tour advances, goes back and finishes ($brightness)', ( tester, ) async { await openTour(tester, brightness: brightness); expect(find.text('Welcome to\nSpotiFLAC-Mobile 5.0'), findsOneWidget); expect(find.byType(Image), findsNothing); await capturePage(tester, '${brightness.name}-welcome'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect(find.text('Make it\nyour own.'), findsOneWidget); await capturePage(tester, '${brightness.name}-themes'); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Welcome to\nSpotiFLAC-Mobile 5.0'), findsOneWidget); for (var i = 0; i < 4; i++) { await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); if (i == 1) { expect(find.text('Get to know\nyour Library.'), findsOneWidget); await capturePage(tester, '${brightness.name}-library'); } if (i == 2) { expect(find.text('A new engine.\nBuilt with Rust.'), findsOneWidget); await capturePage(tester, '${brightness.name}-rust'); } } await capturePage(tester, '${brightness.name}-player'); await tester.tap(find.text('Start listening')); await tester.pumpAndSettle(); expect(find.byType(UpgradeIntroScreen), findsNothing); expect(find.text('Open tour'), findsOneWidget); expect(tester.takeException(), isNull); }); } testWidgets('theme choice applies live, persists and updates the guide', ( tester, ) async { await openTour(tester, style: AppThemeStyle.material); await capturePage(tester, 'material-welcome'); final prefs = await SharedPreferences.getInstance(); expect(prefs.getString(kThemeStyleKey), 'material'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); final mornye = find.byKey(const ValueKey('intro-theme-mornye')); await tester.ensureVisible(mornye); await tester.tap(mornye); await tester.pumpAndSettle(); expect(prefs.getString(kThemeStyleKey), 'mornye'); expect(tester.element(find.byType(UpgradeIntroScreen)).isMornye, isTrue); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect(find.textContaining('Recently Added.'), findsWidgets); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); final material = find.byKey(const ValueKey('intro-theme-material')); await tester.ensureVisible(material); await tester.tap(material); await tester.pumpAndSettle(); expect(prefs.getString(kThemeStyleKey), 'material'); expect(tester.element(find.byType(UpgradeIntroScreen)).isMornye, isFalse); await capturePage(tester, 'material-themes'); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect(find.text('A new engine.\nBuilt with Rust.'), findsOneWidget); expect(find.text('Get to know\nyour Library.'), findsNothing); expect(find.text('Set up\nyour player.'), findsNothing); expect(find.bySemanticsLabel('3 / 3'), findsOneWidget); expect(find.text('Continue'), findsNothing); await capturePage(tester, 'material-rust'); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Make it\nyour own.'), findsOneWidget); await tester.drag(find.byType(PageView), const Offset(-500, 0)); await tester.pumpAndSettle(); expect(find.text('A new engine.\nBuilt with Rust.'), findsOneWidget); await tester.tap(find.text('Start listening')); await tester.pumpAndSettle(); expect(prefs.getString(kThemeStyleKey), 'material'); expect(tester.takeException(), isNull); }); for (final size in [const Size(320, 568), const Size(844, 390)]) { testWidgets( 'tour scrolls with large text and can skip from any page ($size)', (tester) async { await openTour(tester, size: size, textScale: 2); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); await tester.drag( find.byType(SingleChildScrollView).first, const Offset(0, -400), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); await tester.tap(find.text('Skip')); await tester.pumpAndSettle(); expect(find.byType(UpgradeIntroScreen), findsNothing); }, ); } }