build: Add maestro based integration tests
This commit is contained in:
parent
55a886c68a
commit
23f97df1f1
15 changed files with 351 additions and 631 deletions
|
|
@ -1,173 +0,0 @@
|
|||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:fluffychat/main.dart' as app;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'extensions/default_flows.dart';
|
||||
import 'extensions/wait_for.dart';
|
||||
import 'users.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Integration Test', () {
|
||||
setUpAll(() {
|
||||
// this random dialog popping up is super hard to cover in tests
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'chat.fluffy.show_no_google': false,
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Start app, login and logout', (WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.ensureAppStartedHomescreen();
|
||||
await tester.ensureLoggedOut();
|
||||
});
|
||||
|
||||
testWidgets('Login again', (WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.ensureAppStartedHomescreen();
|
||||
});
|
||||
|
||||
testWidgets('Start chat and send message', (WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.ensureAppStartedHomescreen();
|
||||
await tester.waitFor(find.byType(TextField));
|
||||
await tester.enterText(find.byType(TextField), Users.user2.name);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Chats').first,
|
||||
500,
|
||||
scrollable: find
|
||||
.descendant(
|
||||
of: find.byType(ChatListViewBody),
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Chats'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.waitFor(find.byType(SearchTitle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(Users.user2.name).first,
|
||||
500,
|
||||
scrollable: find
|
||||
.descendant(
|
||||
of: find.byType(ChatListViewBody),
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(Users.user2.name).first);
|
||||
|
||||
try {
|
||||
await tester.waitFor(
|
||||
find.byType(ChatView),
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
} catch (_) {
|
||||
// in case the homeserver sends the username as search result
|
||||
if (find.byIcon(Icons.send_outlined).evaluate().isNotEmpty) {
|
||||
await tester.tap(find.byIcon(Icons.send_outlined));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
await tester.waitFor(find.byType(ChatView));
|
||||
await tester.enterText(find.byType(TextField).last, 'Test');
|
||||
await tester.pumpAndSettle();
|
||||
try {
|
||||
await tester.waitFor(find.byIcon(Icons.send_outlined));
|
||||
await tester.tap(find.byIcon(Icons.send_outlined));
|
||||
} catch (_) {
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
await tester.waitFor(find.text('Test'));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('Spaces', (tester) async {
|
||||
app.main();
|
||||
await tester.ensureAppStartedHomescreen();
|
||||
|
||||
await tester.waitFor(find.byTooltip('Show menu'));
|
||||
await tester.tap(find.byTooltip('Show menu'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.byIcon(Icons.workspaces_outlined));
|
||||
await tester.tap(find.byIcon(Icons.workspaces_outlined));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.byType(TextField));
|
||||
await tester.enterText(find.byType(TextField).last, 'Test Space');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.text('Invite contact'));
|
||||
|
||||
await tester.tap(find.text('Invite contact'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(
|
||||
find.descendant(
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
);
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
Users.user2.name,
|
||||
);
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 250));
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(
|
||||
find
|
||||
.descendant(
|
||||
of: find.descendant(
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(ListTile),
|
||||
),
|
||||
matching: find.text(Users.user2.name),
|
||||
)
|
||||
.last,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.maybeUppercaseText('Yes'));
|
||||
await tester.tap(find.maybeUppercaseText('Yes'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.text('Load 2 more participants'));
|
||||
await tester.tap(find.text('Load 2 more participants'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(Users.user2.name), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
5
integration_test/data/integration_users.env
Normal file
5
integration_test/data/integration_users.env
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
HOMESERVER=localhost
|
||||
USER1_NAME=alice
|
||||
USER1_PW=AliceInWonderland
|
||||
USER2_NAME=bob
|
||||
USER2_PW=JoWirSchaffenDas
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
|
||||
import 'package:fluffychat/pages/intro/intro_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../users.dart';
|
||||
import 'wait_for.dart';
|
||||
|
||||
extension DefaultFlowExtensions on WidgetTester {
|
||||
Future<void> login() async {
|
||||
final tester = this;
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.text('Let\'s start'));
|
||||
|
||||
expect(find.text('Let\'s start'), findsOneWidget);
|
||||
|
||||
final input = find.byType(TextField);
|
||||
|
||||
expect(input, findsOneWidget);
|
||||
|
||||
// getting the placeholder in place
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(input, homeserver);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// in case registration is allowed
|
||||
// try {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Login'),
|
||||
500,
|
||||
scrollable: find.descendant(
|
||||
of: find.byKey(const Key('ConnectPageListView')),
|
||||
matching: find.byType(Scrollable).first,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Login'));
|
||||
await tester.pumpAndSettle();
|
||||
/*} catch (e) {
|
||||
log('Registration is not allowed. Proceeding with login...');
|
||||
}*/
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final inputs = find.byType(TextField);
|
||||
|
||||
await tester.enterText(inputs.first, Users.user1.name);
|
||||
await tester.enterText(inputs.last, Users.user1.password);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
|
||||
try {
|
||||
// pumpAndSettle does not work in here as setState is called
|
||||
// asynchronously
|
||||
await tester.waitFor(
|
||||
find.byType(LinearProgressIndicator),
|
||||
timeout: const Duration(milliseconds: 1500),
|
||||
skipPumpAndSettle: true,
|
||||
);
|
||||
} catch (_) {
|
||||
// in case the input action does not work on the desired platform
|
||||
if (find.text('Login').evaluate().isNotEmpty) {
|
||||
await tester.tap(find.text('Login'));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await tester.pumpAndSettle();
|
||||
} catch (_) {
|
||||
// may fail because of ongoing animation below dialog
|
||||
}
|
||||
|
||||
await tester.waitFor(
|
||||
find.byType(ChatListViewBody),
|
||||
skipPumpAndSettle: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// ensure PushProvider check passes
|
||||
Future<void> acceptPushWarning() async {
|
||||
final tester = this;
|
||||
|
||||
final matcher = find.maybeUppercaseText('Do not show again');
|
||||
|
||||
try {
|
||||
await tester.waitFor(matcher, timeout: const Duration(seconds: 5));
|
||||
|
||||
// the FCM push error dialog to be handled...
|
||||
await tester.tap(matcher);
|
||||
await tester.pumpAndSettle();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> ensureLoggedOut() async {
|
||||
final tester = this;
|
||||
await tester.pumpAndSettle();
|
||||
if (find.byType(ChatListViewBody).evaluate().isNotEmpty) {
|
||||
await tester.tap(find.byTooltip('Show menu'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Settings'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Account'),
|
||||
500,
|
||||
scrollable: find.descendant(
|
||||
of: find.byKey(const Key('SettingsListViewContent')),
|
||||
matching: find.byType(Scrollable),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Logout'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.maybeUppercaseText('Yes'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> ensureAppStartedHomescreen({
|
||||
Duration timeout = const Duration(seconds: 20),
|
||||
}) async {
|
||||
final tester = this;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final homeserverPickerFinder = find.byType(IntroPage);
|
||||
final chatListFinder = find.byType(ChatListViewBody);
|
||||
|
||||
final end = DateTime.now().add(timeout);
|
||||
|
||||
log(
|
||||
'Waiting for HomeserverPicker or ChatListViewBody...',
|
||||
name: 'Test Runner',
|
||||
);
|
||||
do {
|
||||
if (DateTime.now().isAfter(end)) {
|
||||
throw Exception(
|
||||
'Timed out waiting for HomeserverPicker or ChatListViewBody',
|
||||
);
|
||||
}
|
||||
|
||||
await pumpAndSettle();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
} while (homeserverPickerFinder.evaluate().isEmpty &&
|
||||
chatListFinder.evaluate().isEmpty);
|
||||
|
||||
if (homeserverPickerFinder.evaluate().isNotEmpty) {
|
||||
log('Found HomeserverPicker, performing login.', name: 'Test Runner');
|
||||
await tester.login();
|
||||
} else {
|
||||
log('Found ChatListViewBody, skipping login.', name: 'Test Runner');
|
||||
}
|
||||
|
||||
await tester.acceptPushWarning();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// Workaround for https://github.com/flutter/flutter/issues/88765
|
||||
extension WaitForExtension on WidgetTester {
|
||||
Future<void> waitFor(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 20),
|
||||
bool skipPumpAndSettle = false,
|
||||
}) async {
|
||||
final end = DateTime.now().add(timeout);
|
||||
|
||||
do {
|
||||
if (DateTime.now().isAfter(end)) {
|
||||
throw Exception('Timed out waiting for $finder');
|
||||
}
|
||||
|
||||
if (!skipPumpAndSettle) {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
} while (finder.evaluate().isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
extension MaybeUppercaseFinder on CommonFinders {
|
||||
/// On Android some button labels are in uppercase while on iOS they
|
||||
/// are not. This method tries both.
|
||||
Finder maybeUppercaseText(
|
||||
String text, {
|
||||
bool findRichText = false,
|
||||
bool skipOffstage = true,
|
||||
}) {
|
||||
try {
|
||||
final finder = find.text(
|
||||
text.toUpperCase(),
|
||||
findRichText: findRichText,
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
return finder;
|
||||
} catch (_) {
|
||||
return find.text(
|
||||
text,
|
||||
findRichText: findRichText,
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
integration_test/login.yaml
Normal file
31
integration_test/login.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
appId: chat.fluffy.fluffychat
|
||||
---
|
||||
- assertVisible: "Sign in"
|
||||
- tapOn: "Sign in"
|
||||
- tapOn: "Search or enter homeserver address"
|
||||
- inputText: "http://${HOMESERVER}"
|
||||
- pressKey: "back"
|
||||
- tapOn:
|
||||
id: "homeserver_tile_0"
|
||||
- tapOn:
|
||||
id: "connect_to_homeserver_button"
|
||||
- assertVisible: "Log in to http://${HOMESERVER}"
|
||||
- inputText: "${USER1_NAME}"
|
||||
- tapOn: "Password"
|
||||
- inputText: "${USER1_PW}"
|
||||
- tapOn: "Login" # Click the login button
|
||||
- tapOn:
|
||||
id: "store_in_secure_storage"
|
||||
- tapOn: "Next"
|
||||
- tapOn:
|
||||
text: "Close"
|
||||
index: 1
|
||||
- assertVisible: "Push notifications not available"
|
||||
- tapOn: "Do not show again"
|
||||
- tapOn:
|
||||
id: "accounts_and_settings" # Open the popup menu
|
||||
- tapOn: "Settings"
|
||||
- scrollUntilVisible:
|
||||
element: "Logout"
|
||||
- tapOn: "Logout"
|
||||
- tapOn: "Logout" # Confirm logout dialog
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
abstract class Users {
|
||||
const Users._();
|
||||
|
||||
static const user1 = User(
|
||||
String.fromEnvironment(
|
||||
'USER1_NAME',
|
||||
defaultValue: 'alice',
|
||||
),
|
||||
String.fromEnvironment(
|
||||
'USER1_PW',
|
||||
defaultValue: 'AliceInWonderland',
|
||||
),
|
||||
);
|
||||
static const user2 = User(
|
||||
String.fromEnvironment(
|
||||
'USER2_NAME',
|
||||
defaultValue: 'bob',
|
||||
),
|
||||
String.fromEnvironment(
|
||||
'USER2_PW',
|
||||
defaultValue: 'JoWirSchaffenDas',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class User {
|
||||
final String name;
|
||||
final String password;
|
||||
|
||||
const User(this.name, this.password);
|
||||
}
|
||||
|
||||
const homeserver = 'http://${String.fromEnvironment(
|
||||
'HOMESERVER',
|
||||
defaultValue: 'localhost',
|
||||
)}';
|
||||
Loading…
Add table
Add a link
Reference in a new issue