Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions app/integration_test/app_updates_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:papyrus/themes/app_theme.dart';
import 'package:papyrus/updates/update_controller.dart';
import 'package:papyrus/updates/update_release.dart';
import 'package:papyrus/widgets/profile/about_settings_section.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('update settings navigation and states on mobile', (tester) async {
for (final (name, theme, scale) in [
('light', AppTheme.light, 1.0),
('dark', AppTheme.dark, 1.0),
('large-text', AppTheme.dark, 2.0),
('eink', AppTheme.eink, 1.0),
]) {
SharedPreferences.setMockInitialValues({});
var result = Completer<Map<String, dynamic>>();
final installation = Completer<String>();
final updates = UpdateController(
preferences: await SharedPreferences.getInstance(),
appInfo: () async =>
PackageInfo(appName: 'Papyrus', packageName: 'papyrus', version: '0.3.0', buildNumber: '7'),
channelProvider: () async => UpdateChannel.windows,
manifest: () => result.future,
installer: (_, progress) async {
progress(0.5);
return installation.future;
},
);

await tester.pumpWidget(
ChangeNotifierProvider.value(
value: updates,
child: MaterialApp(
key: ValueKey(name),
debugShowCheckedModeBanner: false,
theme: theme,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(scale)),
child: child!,
),
home: const Scaffold(body: SafeArea(child: AboutSettingsSection())),
),
),
);

await tester.pumpAndSettle();
await tester.tap(find.text('App updates'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
expect(find.text('Not checked yet'), findsOneWidget);
await binding.takeScreenshot('updates-$name-unchecked');
await tester.tap(find.text('Check automatically'));
await tester.pumpAndSettle();
expect(updates.automatic, isFalse);

if (name == 'dark') {
await tester.tap(find.text('Check for updates'));
await tester.pump(const Duration(milliseconds: 300));
await binding.takeScreenshot('updates-dark-checking');
result.completeError(StateError('Offline'));
await tester.pumpAndSettle();
expect(find.text('Couldn’t check for updates'), findsOneWidget);
await tester.pump(kThemeChangeDuration);
await binding.takeScreenshot('updates-dark-error');
final retryButton = find.byKey(const ValueKey('check-updates'));
final retryBounds = tester.getRect(retryButton);
final toggleBounds = tester.getRect(find.byType(Switch));
result = Completer<Map<String, dynamic>>();
await tester.tap(find.text('Retry'));
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Retry'), findsOneWidget);
expect(find.text('Please try again.'), findsNothing);
expect(find.byType(LinearProgressIndicator), findsNothing);
expect(tester.getRect(retryButton), retryBounds);
expect(tester.getRect(find.byType(Switch)), toggleBounds);
result.completeError(StateError('Still offline'));
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 350));
expect(find.byType(LinearProgressIndicator), findsNothing);
expect(tester.getRect(retryButton), retryBounds);
await binding.takeScreenshot('updates-dark-quick-retry');
result = Completer<Map<String, dynamic>>();
await tester.tap(find.text('Retry'));
await tester.pump();
result.complete(_release(7));
await tester.pumpAndSettle();
expect(find.text('You’re up to date'), findsOneWidget);
await binding.takeScreenshot('updates-dark-current');
result = Completer<Map<String, dynamic>>();
await tester.tap(find.text('Check for updates'));
await tester.pump();
result.complete(_release(8));
await tester.pumpAndSettle();
await binding.takeScreenshot('updates-dark-available');
await tester.tap(find.text('Update'));
await tester.pump(const Duration(milliseconds: 300));
await binding.takeScreenshot('updates-dark-progress');
installation.complete('Close Papyrus normally to install the update.');
await tester.pumpAndSettle();
await binding.takeScreenshot('updates-dark-staged');
}

expect(tester.takeException(), isNull);

await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Version'), findsOneWidget);
expect(updates.viewingSettings, isFalse);
await tester.pumpWidget(const SizedBox.shrink());
updates.dispose();
}
});
}

Map<String, dynamic> _release(int build) => {
'tag': 'v0.3.0+$build',
'version': '0.3.0',
'build_number': build,
'assets': {
'installer': {'name': 'papyrus-v0.3.0+$build-windows-x64-setup.exe', 'sha256': 'a' * 64},
},
};
117 changes: 117 additions & 0 deletions app/integration_test/mobile_settings_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:integration_test/integration_test.dart';
import 'package:papyrus/main.dart' as app;
import 'package:papyrus/pages/profile_page.dart';
import 'package:papyrus/pages/licenses_page.dart';
import 'package:papyrus/providers/preferences_provider.dart';
import 'package:papyrus/widgets/settings/settings_row.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('mobile settings groups, saved preferences, and native Back', (tester) async {
SharedPreferences.setMockInitialValues({'offline_mode': true, 'theme_mode': 'light'});
await app.main();
await tester.pumpAndSettle();
final router = GoRouter.of(tester.element(find.byType(Scaffold).first));
router.go('/profile');
await tester.pumpAndSettle();
final preferences = tester.element(find.byType(ProfilePage)).read<PreferencesProvider>();
await binding.takeScreenshot('settings-index-light');

for (final (title, control) in [
('Account', 'Display name'),
('Appearance', 'Theme'),
('Storage', 'Clear local library'),
('Accessibility', 'Reduce animations'),
('About', 'Version'),
]) {
await tester.ensureVisible(find.widgetWithText(ListTile, title));
await tester.tap(find.widgetWithText(ListTile, title));
await tester.pumpAndSettle();
expect(find.text(control), findsOneWidget);
final route = ModalRoute.of(tester.element(find.byType(ProfilePage).last))! as TransitionRoute<dynamic>;
expect(route.transitionDuration, Duration.zero);
expect(route.reverseTransitionDuration, Duration.zero);
await binding.takeScreenshot('settings-${title.toLowerCase()}-light');

if (title == 'About') {
await tester.tap(find.widgetWithText(SettingsRow, 'Licenses'));

for (var frame = 0; frame < 300; frame++) {
await tester.pump(const Duration(milliseconds: 16));

if (find.byType(LicensesPage).evaluate().isNotEmpty) {
break;
}
}

expect(find.byType(LicensesPage), findsOneWidget);
expect(find.byType(ListTile), findsWidgets);
expect(find.text('Loading licenses…'), findsNothing);
await binding.takeScreenshot('settings-licenses-first-frame');
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Version'), findsOneWidget);
}

await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(router.routeInformationProvider.value.uri.path, '/profile');
}

await tester.tap(find.widgetWithText(ListTile, 'Appearance'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(SettingsRow, 'Theme'));
await tester.pumpAndSettle();
await tester.tap(find.text('Dark'));
await tester.pumpAndSettle();
expect(preferences.themeModePref, 'dark');
await binding.takeScreenshot('settings-appearance-dark');

await tester.binding.handlePopRoute();
await tester.pumpAndSettle();

expect(router.routeInformationProvider.value.uri.path, '/profile');
expect(find.text('Dark theme, page layout'), findsOneWidget);
tester.state<ScrollableState>(find.byType(Scrollable).first).position.jumpTo(0);
await tester.pumpAndSettle();
await binding.takeScreenshot('settings-index-dark');
preferences.interfaceTextEnlargement = 2;
await tester.pumpAndSettle();
await binding.takeScreenshot('settings-index-large-text');
await tester.ensureVisible(find.widgetWithText(ListTile, 'Accessibility'));
await tester.tap(find.widgetWithText(ListTile, 'Accessibility'));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings-accessibility-large-text');
expect(tester.takeException(), isNull);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
preferences.interfaceTextEnlargement = 1;
preferences.reduceAnimations = true;
await tester.pumpAndSettle();
router.go('/profile/appearance');
await tester.pumpAndSettle();
final route = ModalRoute.of(tester.element(find.byType(ProfilePage).last))! as TransitionRoute<dynamic>;
expect(route.transitionDuration, Duration.zero);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(router.routeInformationProvider.value.uri.path, '/profile');
preferences.reduceAnimations = false;
preferences.themeModePref = 'eink';
await tester.pumpAndSettle();
tester.state<ScrollableState>(find.byType(Scrollable).first).position.jumpTo(0);
await tester.pumpAndSettle();
await binding.takeScreenshot('settings-index-eink');
await tester.tap(find.widgetWithText(ListTile, 'Appearance'));
await tester.pump();
expect(find.text('Theme'), findsOneWidget);
await tester.pumpAndSettle();
await binding.takeScreenshot('settings-appearance-eink');
expect(tester.takeException(), isNull);
});
}
14 changes: 13 additions & 1 deletion app/lib/config/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:papyrus/pages/profile_page.dart';
import 'package:papyrus/pages/licenses_page.dart';
import 'package:papyrus/pages/support_page.dart';
import 'package:papyrus/services/support_api_client.dart';
import 'package:papyrus/services/license_packages.dart';
import 'package:papyrus/pages/register_page.dart';
import 'package:papyrus/pages/reader_page.dart';
import 'package:papyrus/pages/search_options_page.dart';
Expand Down Expand Up @@ -289,9 +290,20 @@ class AppRouter {
path: '/profile',
pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const ProfilePage()),
routes: [
for (final section in ProfileSection.values)
GoRoute(
path: section.path,
pageBuilder: (context, state) => NoTransitionPage(
key: state.pageKey,
child: ProfilePage(section: section),
),
),
GoRoute(
path: 'licenses',
pageBuilder: (context, state) => NoTransitionPage(key: state.pageKey, child: const LicensesPage()),
pageBuilder: (context, state) => NoTransitionPage(
key: state.pageKey,
child: LicensesPage(initialLicenses: state.extra as LicensePackages?),
),
),
GoRoute(
path: 'support',
Expand Down
Loading
Loading