A framework for building convergent cross-platform Nextcloud clients using Flutter.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

575 lines
17 KiB

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/apps/files/app.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/blocs/push_notifications.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:settings/settings.dart';
import 'package:shared_preferences/shared_preferences.dart';
class MemorySharedPreferences implements SharedPreferences {
final _data = <String, dynamic>{};
@override
Future<bool> clear() async {
_data.clear();
return true;
}
@override
Future<bool> commit() async => true;
@override
Future reload() async {}
@override
Future<bool> remove(final String key) async {
_data.remove(key);
return true;
}
@override
Set<String> getKeys() => _data.keys.toSet();
@override
bool containsKey(final String key) => _data.keys.contains(key);
@override
Object? get(final String key) => _data[key];
@override
bool? getBool(final String key) => _data[key] as bool?;
@override
double? getDouble(final String key) => _data[key] as double?;
@override
int? getInt(final String key) => _data[key] as int?;
@override
String? getString(final String key) => _data[key] as String?;
@override
List<String>? getStringList(final String key) => (_data[key] as List).cast<String>();
@override
Future<bool> setBool(final String key, final bool value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setDouble(final String key, final double value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setInt(final String key, final int value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setString(final String key, final String value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setStringList(final String key, final List<String> value) async {
_data[key] = value;
return true;
}
}
Future pumpAppPage(
final WidgetTester tester,
final IntegrationTestWidgetsFlutterBinding binding,
final PackageInfo packageInfo, {
required final Widget Function(BuildContext, Function(NextcloudTheme)) builder,
final Account? account,
}) async {
final sharedPreferences = MemorySharedPreferences();
final platform = await getNeonPlatform();
final requestManager = RequestManager();
final allAppImplementations = getAppImplementations(sharedPreferences, requestManager, platform);
final globalOptions = GlobalOptions(
Storage('global', sharedPreferences),
packageInfo,
);
await globalOptions.pushNotificationsEnabled.set(false);
final accountsBloc = AccountsBloc(
requestManager,
platform,
Storage('accounts', sharedPreferences),
sharedPreferences,
globalOptions,
packageInfo,
allAppImplementations,
);
if (account != null) {
accountsBloc.addAccount(account);
}
final pushNotificationsBloc = PushNotificationsBloc(
accountsBloc,
sharedPreferences,
globalOptions,
null,
platform,
);
// ignore: close_sinks
final userThemeStream = BehaviorSubject<NextcloudTheme>();
await tester.pumpWidget(
MultiProvider(
providers: [
Provider<SharedPreferences>(
create: (final _) => sharedPreferences,
),
Provider<Env?>(
create: (final _) => null,
),
Provider<NeonPlatform>(
create: (final _) => platform,
),
Provider<GlobalOptions>(
create: (final _) => globalOptions,
),
Provider<RequestManager>(
create: (final _) => requestManager,
),
Provider<AccountsBloc>(
create: (final _) => accountsBloc,
),
Provider<PushNotificationsBloc>(
create: (final _) => pushNotificationsBloc,
),
Provider<List<AppImplementation>>(
create: (final _) => allAppImplementations,
),
Provider<PackageInfo>(
create: (final _) => packageInfo,
),
],
child: StreamBuilder<NextcloudTheme>(
stream: userThemeStream,
builder: (final context, final themeSnapshot) => OptionBuilder(
option: globalOptions.themeMode,
builder: (final context, final themeMode) => OptionBuilder(
option: globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDark) => OptionBuilder(
option: globalOptions.themeKeepOriginalAccentColor,
builder: (final context, final themeKeepOriginalAccentColor) => MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: getThemeFromNextcloudTheme(
themeSnapshot.data,
themeMode ?? ThemeMode.system,
Brightness.light,
oledAsDark: themeOLEDAsDark ?? false,
keepOriginalAccentColor: themeKeepOriginalAccentColor ?? true,
),
home: builder(context, userThemeStream.add),
),
),
),
),
),
),
);
await tester.pumpAndSettle();
}
Future openDrawer(final WidgetTester tester) async {
await tester.tap(find.byTooltip('Open navigation menu'));
await tester.pumpAndSettle();
}
Future switchPage(final WidgetTester tester, final String name) async {
await openDrawer(tester);
await tester.tap(find.byKey(Key(name)));
await tester.pumpAndSettle();
}
Future prepareScreenshot(final WidgetTester tester, final IntegrationTestWidgetsFlutterBinding binding) async {
await binding.convertFlutterSurfaceToImage();
await tester.pumpAndSettle();
}
Future main() async {
// The screenshots are pretty annoying on Android. See https://github.com/flutter/flutter/issues/92381
assert(Platform.isAndroid, 'Screenshots need to be taken on Android');
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
late PackageInfo packageInfo;
late Account account;
setUpAll(() async {
packageInfo = await PackageInfo.fromPlatform();
account = Account(
serverURL: 'http://10.0.2.2',
username: 'user1',
password: 'user1',
)..setupClient(packageInfo);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
});
testWidgets('login', (final tester) async {
await pumpAppPage(
tester,
binding,
packageInfo,
builder: (final context, final _) => const LoginPage(),
);
await prepareScreenshot(tester, binding);
await binding.takeScreenshot('login_server_selection');
await tester.enterText(find.byType(TextFormField), account.serverURL);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 3)); // Make sure the login webview is loaded
await tester.pumpAndSettle();
await binding.takeScreenshot('login_form');
});
testWidgets('home', (final tester) async {
await pumpAppPage(
tester,
binding,
packageInfo,
account: account,
builder: (final context, final onThemeChanged) => HomePage(
account: account,
onThemeChanged: onThemeChanged,
),
);
await openDrawer(tester);
await tester.pumpAndSettle();
await tester.pump();
await prepareScreenshot(tester, binding);
await binding.takeScreenshot('home_drawer');
});
testWidgets('files', (final tester) async {
await pumpAppPage(
tester,
binding,
packageInfo,
account: account,
builder: (final context, final onThemeChanged) => HomePage(
account: account,
onThemeChanged: onThemeChanged,
),
);
await prepareScreenshot(tester, binding);
await binding.takeScreenshot('files_root');
// Show Photos folder
await tester.tap(find.text('Photos'));
await tester.pumpAndSettle();
await tester.pump();
await binding.takeScreenshot('files_photos');
// Show file actions
await tester.tap(find.text('Photos'));
await tester.pumpAndSettle();
await tester.tap(find.byType(PopupMenuButton<FilesFileAction>).first);
await tester.pumpAndSettle();
await binding.takeScreenshot('files_actions');
// Show details page
await tester.tap(find.text('Details'));
await tester.pumpAndSettle();
await binding.takeScreenshot('files_details');
// Show create dialog
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await binding.takeScreenshot('files_create');
});
testWidgets('news', (final tester) async {
const wikipediaFeedURL = 'https://en.wikipedia.org/w/api.php?action=featuredfeed&feed=featured&feedformat=atom';
const nasaFeedURL = 'https://www.nasa.gov/rss/dyn/breaking_news.rss';
final folder = await account.client.news.createFolder(name: 'test');
await account.client.news.addFeed(
url: nasaFeedURL,
folderId: folder.folders.single.id,
);
await pumpAppPage(
tester,
binding,
packageInfo,
account: account,
builder: (final context, final onThemeChanged) => HomePage(
account: account,
onThemeChanged: onThemeChanged,
),
);
await prepareScreenshot(tester, binding);
await switchPage(tester, 'app-news');
// Show folders
await tester.tap(find.byIcon(Icons.folder));
await tester.pumpAndSettle();
await binding.takeScreenshot('news_folders_list');
// Add Wikipedia feed
await tester.tap(find.byIcon(Icons.rss_feed));
await tester.pumpAndSettle();
await tester.pump();
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await binding.takeScreenshot('news_feed_add');
// Finish adding Wikipedia feed
await tester.enterText(find.byType(TextFormField), wikipediaFeedURL);
await tester.pumpAndSettle();
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 3));
await tester.pumpAndSettle();
await tester.pump();
await binding.takeScreenshot('news_feeds_list');
// Open feed
await tester.tap(find.text('NASA Breaking News'));
await tester.pumpAndSettle();
await binding.takeScreenshot('news_feed_articles_list');
// Show unread articles
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.newspaper));
await tester.pumpAndSettle();
// Star two articles
await tester.tap(find.byIcon(Icons.star_outline).at(0));
await tester.tap(find.byIcon(Icons.star_outline).at(1));
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 3));
await binding.takeScreenshot('news_articles_unread_list');
// Show starred articles
await tester.tap(find.text('Unread'));
await tester.pumpAndSettle();
await tester.tap(find.text('Starred').last);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 3));
await binding.takeScreenshot('news_articles_starred_list');
});
testWidgets('notes', (final tester) async {
await account.client.notes.createNote(
title: 'Wishlist',
category: 'Financial',
);
await pumpAppPage(
tester,
binding,
packageInfo,
account: account,
builder: (final context, final onThemeChanged) => HomePage(
account: account,
onThemeChanged: onThemeChanged,
),
);
await prepareScreenshot(tester, binding);
await switchPage(tester, 'app-notes');
// Create note
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField).first, 'Grocery');
await tester.pumpAndSettle();
await tester.tap(find.byType(TextFormField).last);
await tester.pumpAndSettle();
await binding.takeScreenshot('notes_note_create');
// Finish creating note
await tester.enterText(find.byType(TextFormField).last, 'Financial');
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 3));
// Star note
await tester.tap(find.byIcon(Icons.star_outline).first);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 3));
await binding.takeScreenshot('notes_notes_list');
// Edit note
await tester.tap(find.text('Grocery'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '- Bread\n- Water\n- Apples');
await tester.pumpAndSettle();
await tester.pump(); // Needed for the text to actually show up
await binding.takeScreenshot('notes_note_edit');
// Show note preview
await tester.tap(find.byIcon(Icons.visibility));
await tester.pumpAndSettle();
await binding.takeScreenshot('notes_note_preview');
// Show categories
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(MdiIcons.tag).last);
await tester.pumpAndSettle();
await binding.takeScreenshot('notes_categories_list');
});
testWidgets('notifications', (final tester) async {
await (Account(
serverURL: 'http://10.0.2.2',
username: 'admin',
password: 'admin',
)..setupClient(packageInfo))
.client
.notifications
.sendAdminNotification(
userId: account.username,
shortMessage: 'Notifications demo',
longMessage: 'This is a notifications demo of the Neon app',
);
await pumpAppPage(
tester,
binding,
packageInfo,
account: account,
builder: (final context, final onThemeChanged) => HomePage(
account: account,
onThemeChanged: onThemeChanged,
),
);
await prepareScreenshot(tester, binding);
await tester.tap(find.byKey(const Key('app-notifications')));
await tester.pumpAndSettle();
await tester.pumpAndSettle();
await tester.pump();
await binding.takeScreenshot('notifications_list');
});
testWidgets('settings', (final tester) async {
await pumpAppPage(
tester,
binding,
packageInfo,
account: account,
builder: (final context, final onThemeChanged) => HomePage(
account: account,
onThemeChanged: onThemeChanged,
),
);
await prepareScreenshot(tester, binding);
await switchPage(tester, 'settings');
// Open Files settings
await tester.tap(find.text('Files'));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_app_files');
// Open News settings
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
await tester.tap(find.text('News'));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_app_news');
// Open Notes settings
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
await tester.tap(find.text('Notes'));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_app_notes');
// Go back to main page
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_light');
// Change to dark theme
await tester.tap(find.text('Automatic'));
await tester.pumpAndSettle();
await tester.tap(find.text('Dark').last);
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_dark');
// Enable OLED theme
await tester.tap(find.byType(CheckboxListTile).first);
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_oled');
// Change to back to light theme
await tester.tap(find.text('Dark'));
await tester.pumpAndSettle();
await tester.tap(find.text('Light').last);
await tester.pumpAndSettle();
// Scroll down to accounts
await tester.drag(find.byType(ListView), const Offset(0, -10000));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_accounts');
// Go to account settings
await tester.tap(find.text('user1@10.0.2.2:80'));
await tester.pumpAndSettle();
await tester.tap(find.text('Automatic'));
await tester.pumpAndSettle();
await binding.takeScreenshot('settings_account');
});
}