mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-10 09:45:14 +00:00
Merge branch 'braid/integration-tests' into 'main'
chore: add integration tests See merge request famedly/fluffychat!1062
This commit is contained in:
commit
374a7b57e2
18 changed files with 491 additions and 103 deletions
|
@ -1,7 +1,9 @@
|
|||
variables:
|
||||
FLUTTER_VERSION: 3.3.9
|
||||
|
||||
image: cirrusci/flutter:${FLUTTER_VERSION}
|
||||
image:
|
||||
name: cirrusci/flutter:${FLUTTER_VERSION}
|
||||
pull_policy: if-not-present
|
||||
|
||||
.shared_windows_runners:
|
||||
tags:
|
||||
|
@ -16,7 +18,7 @@ stages:
|
|||
|
||||
code_analyze:
|
||||
stage: test
|
||||
script: [./scripts/code_analyze.sh]
|
||||
script: [ ./scripts/code_analyze.sh ]
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: code-quality-report.json
|
||||
|
@ -26,13 +28,13 @@ code_analyze:
|
|||
|
||||
widget_test:
|
||||
stage: test
|
||||
script: [flutter test]
|
||||
script: [ flutter test ]
|
||||
tags:
|
||||
- docker
|
||||
- famedly
|
||||
|
||||
# the basic integration test configuration testing FLOSS builds on Synapse
|
||||
.integration_test:
|
||||
integration_test:
|
||||
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/integration/stable:${FLUTTER_VERSION}
|
||||
stage: test
|
||||
services:
|
||||
|
@ -49,15 +51,13 @@ widget_test:
|
|||
FF_NETWORK_PER_BUILD: "true"
|
||||
# Tell docker CLI how to talk to Docker daemon.
|
||||
DOCKER_HOST: tcp://docker:2375/
|
||||
# Use the overlayfs driver for improved performance.
|
||||
DOCKER_DRIVER: overlay2
|
||||
# Use the btrfs driver for improved performance.
|
||||
DOCKER_DRIVER: btrfs
|
||||
# Disable TLS since we're running inside local network.
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
HOMESERVER: "docker"
|
||||
HOMESERVER: docker
|
||||
before_script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
- scripts/integration-prepare-alpine.sh
|
||||
- scripts/integration-prepare-host.sh
|
||||
# create test user environment variables
|
||||
- source scripts/integration-create-environment-variables.sh
|
||||
# create Synapse instance
|
||||
|
@ -65,31 +65,49 @@ widget_test:
|
|||
# properly set the homeserver IP and create test users
|
||||
- scripts/integration-prepare-homeserver.sh
|
||||
script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
- flutter pub get
|
||||
- flutter test integration_test
|
||||
timeout: 20m
|
||||
- scrcpy --no-display --record video.mkv &
|
||||
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
|
||||
after_script:
|
||||
- ffmpeg -i video.mkv -vf scale=iw/2:-2 -crf 40 -b:v 2000k -preset fast video.mp4 || true
|
||||
timeout: 30m
|
||||
retry: 2
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- video.mp4
|
||||
tags:
|
||||
- docker
|
||||
- famedly
|
||||
|
||||
|
||||
# integration tests for Linux builds
|
||||
### disabled because of Linux headless issues
|
||||
.integration_test_linux:
|
||||
extends: .integration_test
|
||||
image: cirrusci/flutter:${FLUTTER_VERSION}
|
||||
extends: integration_test
|
||||
script:
|
||||
- apk add cmake ninja gtk+3.0-dev clang pkgconf xz-dev libsecret-dev jsoncpp-dev
|
||||
- apt-get update
|
||||
- apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libjsoncpp-dev
|
||||
- flutter pub get
|
||||
- flutter test integration_test -d linux
|
||||
- flutter test integration_test -d linux --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
|
||||
after_script: [ ]
|
||||
artifacts:
|
||||
|
||||
# extending the default tests to test the Google-flavored builds
|
||||
.integration_test_proprietary:
|
||||
extends: .integration_test
|
||||
integration_test_proprietary:
|
||||
extends: integration_test
|
||||
script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- flutter pub get
|
||||
- flutter test integration_test
|
||||
- scrcpy --no-display --record video.mkv &
|
||||
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
|
||||
|
||||
.release_mode_launches:
|
||||
release_mode_launches:
|
||||
parallel:
|
||||
matrix:
|
||||
- FLAVOR:
|
||||
|
@ -99,9 +117,9 @@ widget_test:
|
|||
stage: test
|
||||
before_script:
|
||||
- |
|
||||
if [ "$FLAVOR" == "proprietary" ]; then
|
||||
git apply ./scripts/enable-android-google-services.patch
|
||||
fi
|
||||
if [ "$FLAVOR" == "proprietary" ]; then
|
||||
git apply ./scripts/enable-android-google-services.patch
|
||||
fi
|
||||
script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
|
@ -115,8 +133,8 @@ widget_test:
|
|||
build_web:
|
||||
stage: build
|
||||
before_script:
|
||||
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
|
||||
script: [./scripts/build-web.sh]
|
||||
[ sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh ]
|
||||
script: [ ./scripts/build-web.sh ]
|
||||
artifacts:
|
||||
paths:
|
||||
- build/web/
|
||||
|
@ -166,7 +184,7 @@ build_windows:
|
|||
|
||||
build_android_debug:
|
||||
stage: build
|
||||
script: [./scripts/build-android-debug.sh]
|
||||
script: [ ./scripts/build-android-debug.sh ]
|
||||
artifacts:
|
||||
when: on_success
|
||||
paths:
|
||||
|
@ -183,7 +201,7 @@ build_android_apk:
|
|||
before_script:
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- ./scripts/prepare-android-release.sh
|
||||
script: [./scripts/build-android-apk.sh]
|
||||
script: [ ./scripts/build-android-apk.sh ]
|
||||
artifacts:
|
||||
when: on_success
|
||||
paths:
|
||||
|
@ -200,7 +218,7 @@ deploy_playstore_internal:
|
|||
before_script:
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- ./scripts/prepare-android-release.sh
|
||||
script: [./scripts/release-playstore-beta.sh]
|
||||
script: [ ./scripts/release-playstore-beta.sh ]
|
||||
artifacts:
|
||||
when: on_success
|
||||
paths:
|
||||
|
@ -267,7 +285,7 @@ build_linux_x86:
|
|||
[
|
||||
sudo apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install keyboard-configuration -y && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y,
|
||||
]
|
||||
script: [./scripts/build-linux.sh]
|
||||
script: [ ./scripts/build-linux.sh ]
|
||||
tags:
|
||||
- docker
|
||||
- famedly
|
||||
|
@ -278,9 +296,9 @@ build_linux_x86:
|
|||
|
||||
build_linux_arm64:
|
||||
stage: build
|
||||
before_script: [flutter upgrade]
|
||||
script: [./scripts/build-linux.sh]
|
||||
tags: [docker_arm64]
|
||||
before_script: [ flutter upgrade ]
|
||||
script: [ ./scripts/build-linux.sh ]
|
||||
tags: [ docker_arm64 ]
|
||||
only:
|
||||
- main
|
||||
- tags
|
||||
|
@ -292,7 +310,7 @@ build_linux_arm64:
|
|||
|
||||
update_dependencies:
|
||||
stage: build
|
||||
needs: []
|
||||
needs: [ ]
|
||||
tags:
|
||||
- docker
|
||||
only:
|
||||
|
@ -374,7 +392,7 @@ deploy_playstore:
|
|||
before_script:
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- ./scripts/prepare-android-release.sh
|
||||
script: [./scripts/release-playstore.sh]
|
||||
script: [ ./scripts/release-playstore.sh ]
|
||||
resource_group: playstore_release
|
||||
only:
|
||||
- tags
|
||||
|
|
|
@ -1,49 +1,117 @@
|
|||
import 'dart:developer';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hive_flutter/hive_flutter.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', () {
|
||||
testWidgets('Test if the app starts', (WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.pumpAndSettle();
|
||||
group(
|
||||
'Integration Test',
|
||||
() {
|
||||
setUpAll(
|
||||
() async {
|
||||
// this random dialog popping up is super hard to cover in tests
|
||||
SharedPreferences.setMockInitialValues({
|
||||
SettingKeys.showNoGoogle: false,
|
||||
});
|
||||
try {
|
||||
Hive.deleteFromDisk();
|
||||
Hive.initFlutter();
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
testWidgets(
|
||||
'Start app, login and logout',
|
||||
(WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.ensureAppStartedHomescreen();
|
||||
await tester.ensureLoggedOut();
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
testWidgets(
|
||||
'Login again',
|
||||
(WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.ensureAppStartedHomescreen();
|
||||
},
|
||||
);
|
||||
|
||||
expect(find.text('Connect'), findsOneWidget);
|
||||
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();
|
||||
|
||||
final input = find.byType(TextField);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Chats'),
|
||||
500,
|
||||
scrollable: find.descendant(
|
||||
of: find.byType(ChatListViewBody),
|
||||
matching: find.byType(Scrollable),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Chats'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.waitFor(find.byType(SearchTitle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(input, findsOneWidget);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(Users.user2.name).first,
|
||||
500,
|
||||
scrollable: find.descendant(
|
||||
of: find.byType(ChatListViewBody),
|
||||
matching: find.byType(Scrollable),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(Users.user2.name).first);
|
||||
|
||||
await tester.enterText(input, homeserver);
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// in case registration is allowed
|
||||
try {
|
||||
await tester.tap(find.text('Login'));
|
||||
await tester.pumpAndSettle();
|
||||
} catch (e) {
|
||||
log('Registration is not allowed. Proceeding with login...');
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final inputs = find.byType(TextField);
|
||||
|
||||
await tester.enterText(inputs.first, Users.user1.name);
|
||||
await tester.enterText(inputs.last, Users.user1.password);
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
});
|
||||
});
|
||||
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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
182
integration_test/extensions/default_flows.dart
Normal file
182
integration_test/extensions/default_flows.dart
Normal file
|
@ -0,0 +1,182 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||
import 'package:fluffychat/pages/settings_account/settings_account_view.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('Account'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Logout'),
|
||||
500,
|
||||
scrollable: find.descendant(
|
||||
of: find.byType(SettingsAccountView),
|
||||
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(HomeserverPicker);
|
||||
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();
|
||||
}
|
||||
}
|
49
integration_test/extensions/wait_for.dart
Normal file
49
integration_test/extensions/wait_for.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,25 @@
|
|||
import 'dart:io';
|
||||
|
||||
abstract class Users {
|
||||
const Users._();
|
||||
|
||||
static final user1 = User(
|
||||
Platform.environment['USER1_NAME'] ?? 'alice',
|
||||
Platform.environment['USER1_PW'] ?? 'AliceInWonderland',
|
||||
static const user1 = User(
|
||||
String.fromEnvironment(
|
||||
'USER1_NAME',
|
||||
defaultValue: 'alice',
|
||||
),
|
||||
String.fromEnvironment(
|
||||
'USER1_PW',
|
||||
defaultValue: 'AliceInWonderland',
|
||||
),
|
||||
);
|
||||
static final user2 = User(
|
||||
Platform.environment['USER2_NAME'] ?? 'bob',
|
||||
Platform.environment['USER2_PW'] ?? 'JoWirSchaffenDas',
|
||||
static const user2 = User(
|
||||
String.fromEnvironment(
|
||||
'USER2_NAME',
|
||||
defaultValue: 'bob',
|
||||
),
|
||||
String.fromEnvironment(
|
||||
'USER2_PW',
|
||||
defaultValue: 'JoWirSchaffenDas',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -20,5 +30,7 @@ class User {
|
|||
const User(this.name, this.password);
|
||||
}
|
||||
|
||||
final homeserver =
|
||||
'http://${Platform.environment['HOMESERVER'] ?? 'localhost'}';
|
||||
const homeserver = 'http://${const String.fromEnvironment(
|
||||
'HOMESERVER',
|
||||
defaultValue: 'localhost',
|
||||
)}';
|
||||
|
|
|
@ -104,8 +104,13 @@ class ClientChooserButton extends StatelessWidget {
|
|||
.map(
|
||||
(client) => PopupMenuItem(
|
||||
value: client,
|
||||
child: FutureBuilder<Profile>(
|
||||
future: client!.fetchOwnProfile(),
|
||||
child: FutureBuilder<Profile?>(
|
||||
// analyzer does not understand this type cast for error
|
||||
// handling
|
||||
//
|
||||
// ignore: unnecessary_cast
|
||||
future: (client!.fetchOwnProfile() as Future<Profile?>)
|
||||
.onError((e, s) => null),
|
||||
builder: (context, snapshot) => Row(
|
||||
children: [
|
||||
Avatar(
|
||||
|
|
|
@ -28,6 +28,7 @@ class ConnectPageView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
body: ListView(
|
||||
key: const Key('ConnectPageListView'),
|
||||
children: [
|
||||
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
|
||||
Padding(
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/platform_infos.dart';
|
||||
import 'login_view.dart';
|
||||
|
@ -30,7 +31,7 @@ class LoginController extends State<Login> {
|
|||
void toggleShowPassword() =>
|
||||
setState(() => showPassword = !loading && !showPassword);
|
||||
|
||||
void login([_]) async {
|
||||
void login() async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (usernameController.text.isEmpty) {
|
||||
setState(() => usernameError = L10n.of(context)!.pleaseEnterYourUsername);
|
||||
|
@ -48,6 +49,9 @@ class LoginController extends State<Login> {
|
|||
}
|
||||
|
||||
setState(() => loading = true);
|
||||
|
||||
_coolDown?.cancel();
|
||||
|
||||
try {
|
||||
final username = usernameController.text;
|
||||
AuthenticationIdentifier identifier;
|
||||
|
@ -97,8 +101,8 @@ class LoginController extends State<Login> {
|
|||
void _checkWellKnown(String userId) async {
|
||||
if (mounted) setState(() => usernameError = null);
|
||||
if (!userId.isValidMatrixId) return;
|
||||
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
|
||||
try {
|
||||
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
|
||||
var newDomain = Uri.https(userId.domain!, '');
|
||||
Matrix.of(context).getLoginClient().homeserver = newDomain;
|
||||
DiscoveryInformation? wellKnownInformation;
|
||||
|
@ -112,14 +116,11 @@ class LoginController extends State<Login> {
|
|||
// do nothing, newDomain is already set to a reasonable fallback
|
||||
}
|
||||
if (newDomain != oldHomeserver) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
// do nothing if we error, we'll handle it below
|
||||
future: () => Matrix.of(context)
|
||||
.getLoginClient()
|
||||
.checkHomeserver(newDomain)
|
||||
.catchError((e) {}),
|
||||
);
|
||||
Matrix.of(context)
|
||||
.getLoginClient()
|
||||
.checkHomeserver(newDomain)
|
||||
.catchError((e) {});
|
||||
|
||||
if (Matrix.of(context).getLoginClient().homeserver == null) {
|
||||
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
||||
// okay, the server we checked does not appear to be a matrix server
|
||||
|
@ -140,15 +141,18 @@ class LoginController extends State<Login> {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => usernameError = null);
|
||||
usernameError = null;
|
||||
if (mounted) setState(() {});
|
||||
} else {
|
||||
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
||||
if (mounted) {
|
||||
setState(() =>
|
||||
Matrix.of(context).getLoginClient().homeserver = oldHomeserver);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => usernameError = e.toString());
|
||||
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
||||
usernameError = e.toLocalizedString(context);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ class LoginView extends StatelessWidget {
|
|||
controller: controller.passwordController,
|
||||
textInputAction: TextInputAction.go,
|
||||
obscureText: !controller.showPassword,
|
||||
onSubmitted: controller.login,
|
||||
onSubmitted: (_) => controller.login(),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.lock_outlined),
|
||||
errorText: controller.passwordError,
|
||||
|
@ -87,9 +87,7 @@ class LoginView extends StatelessWidget {
|
|||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: controller.loading
|
||||
? null
|
||||
: () => controller.login(context),
|
||||
onPressed: controller.loading ? null : controller.login,
|
||||
icon: const Icon(Icons.login_outlined),
|
||||
label: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
|
|
|
@ -38,6 +38,7 @@ class SettingsView extends StatelessWidget {
|
|||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||
child: ListView(
|
||||
key: const Key('SettingsListViewContent'),
|
||||
children: <Widget>[
|
||||
AnimatedContainer(
|
||||
height: controller.showChatBackupBanner ? 54 : 0,
|
||||
|
|
|
@ -11,6 +11,7 @@ class ContentBanner extends StatelessWidget {
|
|||
final void Function()? onEdit;
|
||||
final Client? client;
|
||||
final double opacity;
|
||||
final WidgetBuilder? placeholder;
|
||||
|
||||
const ContentBanner(
|
||||
{this.mxContent,
|
||||
|
@ -19,6 +20,7 @@ class ContentBanner extends StatelessWidget {
|
|||
this.onEdit,
|
||||
this.client,
|
||||
this.opacity = 0.75,
|
||||
this.placeholder,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
|
@ -54,6 +56,7 @@ class ContentBanner extends StatelessWidget {
|
|||
uri: mxContent,
|
||||
animated: true,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: placeholder,
|
||||
height: 400,
|
||||
width: 800,
|
||||
),
|
||||
|
|
|
@ -327,15 +327,15 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
|||
);
|
||||
|
||||
if (state != LoginState.loggedIn) {
|
||||
widget.router!.currentState!.to(
|
||||
widget.router?.currentState?.to(
|
||||
'/rooms',
|
||||
queryParameters: widget.router!.currentState!.queryParameters,
|
||||
queryParameters: widget.router?.currentState?.queryParameters ?? {},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
widget.router!.currentState!.to(
|
||||
widget.router?.currentState?.to(
|
||||
state == LoginState.loggedIn ? '/rooms' : '/home',
|
||||
queryParameters: widget.router!.currentState!.queryParameters,
|
||||
queryParameters: widget.router?.currentState?.queryParameters ?? {},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import '../utils/localized_exception_extension.dart';
|
|||
class ProfileBottomSheet extends StatelessWidget {
|
||||
final String userId;
|
||||
final BuildContext outerContext;
|
||||
|
||||
const ProfileBottomSheet({
|
||||
required this.userId,
|
||||
required this.outerContext,
|
||||
|
@ -78,6 +79,13 @@ class ProfileBottomSheet extends StatelessWidget {
|
|||
mxContent: profile.avatarUrl,
|
||||
defaultIcon: Icons.account_circle_outlined,
|
||||
client: Matrix.of(context).client,
|
||||
placeholder: (context) => Center(
|
||||
child: Text(
|
||||
userId.localpart ?? userId,
|
||||
style:
|
||||
Theme.of(context).textTheme.headline3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
import 'package:system_theme/system_theme.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
class ThemeBuilder extends StatefulWidget {
|
||||
final Widget Function(
|
||||
|
@ -34,6 +35,7 @@ class ThemeController extends State<ThemeBuilder> {
|
|||
Color? _primaryColor;
|
||||
|
||||
ThemeMode get themeMode => _themeMode ?? ThemeMode.system;
|
||||
|
||||
Color? get primaryColor => _primaryColor;
|
||||
|
||||
static ThemeController of(BuildContext context) =>
|
||||
|
@ -87,10 +89,18 @@ class ThemeController extends State<ThemeBuilder> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
Color? get systemAccentColor {
|
||||
final color = SystemTheme.accentColor.accent;
|
||||
if (color == kDefaultSystemAccentColor) return AppConfig.chatColor;
|
||||
return color;
|
||||
Color get systemAccentColor {
|
||||
if (PlatformInfos.isLinux) return AppConfig.chatColor;
|
||||
try {
|
||||
// a bad plugin implementation
|
||||
// https://github.com/bdlukaa/system_theme/issues/10
|
||||
final accentColor = SystemTheme.accentColor;
|
||||
final color = accentColor.accent;
|
||||
if (color == kDefaultSystemAccentColor) return AppConfig.chatColor;
|
||||
return color;
|
||||
} catch (_) {
|
||||
return AppConfig.chatColor;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
apk update && apk add docker drill grep
|
|
@ -33,3 +33,28 @@ echo "Homeserver is up."
|
|||
|
||||
curl -fS --retry 3 -XPOST -d "{\"username\":\"$USER1_NAME\", \"password\":\"$USER1_PW\", \"inhibit_login\":true, \"auth\": {\"type\":\"m.login.dummy\"}}" "http://$HOMESERVER/_matrix/client/r0/register"
|
||||
curl -fS --retry 3 -XPOST -d "{\"username\":\"$USER2_NAME\", \"password\":\"$USER2_PW\", \"inhibit_login\":true, \"auth\": {\"type\":\"m.login.dummy\"}}" "http://$HOMESERVER/_matrix/client/r0/register"
|
||||
|
||||
usertoken1=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/login" -H "Content-Type: application/json" -d "{\"type\": \"m.login.password\", \"identifier\": {\"type\": \"m.id.user\",\"user\": \"$USER1_NAME\"},\"password\":\"$USER1_PW\"}" | jq -r '.access_token')
|
||||
usertoken2=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/login" -H "Content-Type: application/json" -d "{\"type\": \"m.login.password\", \"identifier\": {\"type\": \"m.id.user\",\"user\": \"$USER2_NAME\"},\"password\":\"$USER2_PW\"}" | jq -r '.access_token')
|
||||
|
||||
|
||||
# get usernames' mxids
|
||||
mxid1=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/account/whoami" -H "Authorization: Bearer $usertoken1" | jq -r .user_id)
|
||||
mxid2=$(curl -fS --retry 3 "http://$HOMESERVER/_matrix/client/r0/account/whoami" -H "Authorization: Bearer $usertoken2" | jq -r .user_id)
|
||||
|
||||
# setting the display name to username
|
||||
curl -fS --retry 3 -XPUT -d "{\"displayname\":\"$USER1_NAME\"}" "http://$HOMESERVER/_matrix/client/v3/profile/$mxid1/displayname" -H "Authorization: Bearer $usertoken1"
|
||||
curl -fS --retry 3 -XPUT -d "{\"displayname\":\"$USER2_NAME\"}" "http://$HOMESERVER/_matrix/client/v3/profile/$mxid2/displayname" -H "Authorization: Bearer $usertoken2"
|
||||
|
||||
echo "Set display names"
|
||||
|
||||
# create new room to invite user too
|
||||
roomID=$(curl --retry 3 --silent --fail -XPOST -d "{\"name\":\"$USER2_NAME\", \"is_direct\": true}" "http://$HOMESERVER/_matrix/client/r0/createRoom?access_token=$usertoken2" | jq -r '.room_id')
|
||||
echo "Created room '$roomID'"
|
||||
|
||||
# send message in created room
|
||||
curl --retry 3 --fail --silent -XPOST -d '{"msgtype":"m.text", "body":"joined room successfully"}' "http://$HOMESERVER/_matrix/client/r0/rooms/$roomID/send/m.room.message?access_token=$usertoken2"
|
||||
echo "Sent message"
|
||||
|
||||
curl -fS --retry 3 -XPOST -d "{\"user_id\":\"$mxid1\"}" "http://$HOMESERVER/_matrix/client/r0/rooms/$roomID/invite?access_token=$usertoken2"
|
||||
echo "Invited $USER1_NAME"
|
||||
|
|
6
scripts/integration-prepare-host.sh
Executable file
6
scripts/integration-prepare-host.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
if ! command -v apk &>/dev/null; then
|
||||
apt update && apt install -y -qq docker.io ldnsutils grep scrcpy ffmpeg
|
||||
else
|
||||
apk update && apk add docker drill grep scrcpy ffmpeg
|
||||
fi
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
chmod 777 -R /dev/kvm
|
||||
adb start-server
|
||||
emulator -avd test -no-audio -no-boot-anim -no-window -accel on -gpu swiftshader_indirect
|
||||
emulator -avd test -wipe-data -no-audio -no-boot-anim -no-window -accel on -gpu swiftshader_indirect
|
||||
|
|
Loading…
Reference in a new issue