1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-17 01:06:46 +01:00

Merge branch 'main' of github.com:immich-app/immich

This commit is contained in:
Alex Tran 2023-01-25 10:12:01 -06:00
commit 6acfac9064
No known key found for this signature in database
GPG key ID: E4954BC787B85C8A
17 changed files with 249 additions and 82 deletions

View file

@ -39,3 +39,56 @@ jobs:
- name: Run tests
run: cd web && npm ci && npm run check:all
mobile-unit-tests:
name: Run mobile unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
- name: Run tests
working-directory: ./mobile
run: flutter test
mobile-integration-tests:
name: Run mobile end-to-end integration tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- name: Cache android SDK
uses: actions/cache@v3
id: android-sdk
with:
key: android-sdk
path: |
/usr/local/lib/android/
~/.android
- name: Setup Android SDK
if: steps.android-sdk.outputs.cache-hit != 'true'
uses: android-actions/setup-android@v2
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.10'
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2.27.0
with:
working-directory: ./mobile
api-level: 29
arch: x86_64
profile: pixel
target: default
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
disable-linux-hw-accel: false
script: |
flutter pub get
flutter test integration_test

View file

@ -1,44 +0,0 @@
name: Flutter Integration Tests
on:
push:
branches: [ "main" ]
pull_request:
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- name: Cache android SDK
uses: actions/cache@v3
id: android-sdk
with:
key: android-sdk
path: |
/usr/local/lib/android/
~/.android
- name: Setup Android SDK
if: steps.android-sdk.outputs.cache-hit != 'true'
uses: android-actions/setup-android@v2
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2.27.0
with:
working-directory: ./mobile
api-level: 29
arch: x86_64
profile: pixel
target: default
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
disable-linux-hw-accel: false
script: |
flutter pub get
flutter test integration_test

View file

@ -93,6 +93,9 @@ If you feel like this is the right cause and the app is something you are seeing
- [Monthly donation](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [One-time donation](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via Github Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
# Known Issues

View file

@ -1,5 +1,5 @@
---
sidebar_position: 99
sidebar_position: 70
---
# All-In-One [Community]

View file

@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 30
---
# Docker Compose [Recommended]

View file

@ -0,0 +1,24 @@
---
sidebar_position: 40
---
# Kubernetes
You can deploy Immich on Kubernetes using [the official Helm chart](https://github.com/immich-app/immich-charts/tree/main/charts/apps/immich).
If you want examples of how other people run Immich on Kubernetes, using the official chart or otherwise, you can find them at https://nanne.dev/k8s-at-home-search/#/immich.
:::caution DNS in Alpine containers
Immich makes use of Alpine container images. These can encounter [a DNS resolution bug](https://stackoverflow.com/a/65593511) on Kubernetes clusters if the host
nodes have a search domain set, like:
```
$ cat /etc/resolv.conf
search home.lan
nameserver 192.168.1.1
```
When you encounter this bug, it will cause the immich-microservices to crash on startup because it cannot download
the geocoder data. This can be solved in one of two ways: Either reconfigure your nodes to remove the searchdomain from
`resolv.conf`, or set the `DISABLE_REVERSE_GEOCODING` environment variable for Immich to `true` to disable the geocoder.
:::

View file

@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 50
---
# Portainer

View file

@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 10
---

View file

@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 20
---
# Install Script [Experimental]

View file

@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 60
---
# Unraid

View file

@ -14,6 +14,10 @@ If you feel like this is the right cause and the app is something you see yourse
- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502)
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
## Contributing

View file

@ -110,6 +110,8 @@
"experimental_settings_title": "Experimental",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_building_timeline": "Building the timeline",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"login_form_button_text": "Login",

View file

@ -8,12 +8,11 @@ void main() async {
await ImmichTestHelper.initialize();
group("Login input validation test", () {
immichWidgetTest("Test leading/trailing whitespace", (tester) async {
await ImmichTestLoginHelper.waitForLoginScreen(tester);
await ImmichTestLoginHelper.acknowledgeNewServerVersion(tester);
immichWidgetTest("Test leading/trailing whitespace", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await ImmichTestLoginHelper.enterLoginCredentials(
tester,
await helper.loginHelper.enterCredentials(
email: " demo@immich.app"
);
@ -21,8 +20,7 @@ void main() async {
expect(find.text("login_form_err_leading_whitespace".tr()), findsOneWidget);
await ImmichTestLoginHelper.enterLoginCredentials(
tester,
await helper.loginHelper.enterCredentials(
email: "demo@immich.app "
);
@ -31,12 +29,11 @@ void main() async {
expect(find.text("login_form_err_trailing_whitespace".tr()), findsOneWidget);
});
immichWidgetTest("Test invalid email", (tester) async {
await ImmichTestLoginHelper.waitForLoginScreen(tester);
await ImmichTestLoginHelper.acknowledgeNewServerVersion(tester);
immichWidgetTest("Test invalid email", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await ImmichTestLoginHelper.enterLoginCredentials(
tester,
await helper.loginHelper.enterCredentials(
email: "demo.immich.app"
);

View file

@ -0,0 +1,39 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import '../test_utils/general_helper.dart';
import '../test_utils/login_helper.dart';
void main() async {
await ImmichTestHelper.initialize();
group("Login tests", () {
immichWidgetTest("Test correct credentials", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper
.enterCredentialsOf(LoginCredentials.testInstance);
await helper.loginHelper.pressLoginButton();
await helper.loginHelper.assertLoginSuccess();
});
immichWidgetTest("Test login with wrong password", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentialsOf(
LoginCredentials.testInstanceButWithWrongPassword);
await helper.loginHelper.pressLoginButton();
await helper.loginHelper.assertLoginFailed();
});
immichWidgetTest("Test login with wrong server URL", (tester, helper) async {
await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion();
await helper.loginHelper.enterCredentialsOf(
LoginCredentials.wrongInstanceUrl);
await helper.loginHelper.pressLoginButton();
await helper.loginHelper.assertLoginFailed();
});
});
}

View file

@ -1,14 +1,28 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/main.dart';
import 'package:integration_test/integration_test.dart';
import 'package:meta/meta.dart';
import 'package:immich_mobile/main.dart' as app;
import 'login_helper.dart';
class ImmichTestHelper {
final WidgetTester tester;
ImmichTestHelper(this.tester);
ImmichTestLoginHelper? _loginHelper;
ImmichTestLoginHelper get loginHelper {
_loginHelper ??= ImmichTestLoginHelper(tester);
return _loginHelper!;
}
static Future<IntegrationTestWidgetsFlutterBinding> initialize() async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
@ -32,9 +46,12 @@ class ImmichTestHelper {
}
void immichWidgetTest(String description, Future<void> Function(WidgetTester) test) {
testWidgets(description, (widgetTester) async {
await ImmichTestHelper.loadApp(widgetTester);
await test(widgetTester);
});
@isTest
void immichWidgetTest(String description, Future<void> Function(WidgetTester, ImmichTestHelper) test) {
testWidgets(description, (widgetTester) async {
await ImmichTestHelper.loadApp(widgetTester);
await test(widgetTester, ImmichTestHelper(widgetTester));
}, semanticsEnabled: false);
}

View file

@ -1,10 +1,15 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
class ImmichTestLoginHelper {
static Future<void> waitForLoginScreen(WidgetTester tester,
{int timeoutSeconds = 20}) async {
final WidgetTester tester;
ImmichTestLoginHelper(this.tester);
Future<void> waitForLoginScreen({int timeoutSeconds = 20}) async {
for (var i = 0; i < timeoutSeconds; i++) {
// Search for "IMMICH" test in the app bar
final result = find.text("IMMICH");
@ -21,7 +26,7 @@ class ImmichTestLoginHelper {
fail("Timeout while waiting for login screen");
}
static Future<bool> acknowledgeNewServerVersion(WidgetTester tester) async {
Future<bool> acknowledgeNewServerVersion() async {
final result = find.text("Acknowledge");
if (!tester.any(result)) {
return false;
@ -33,8 +38,7 @@ class ImmichTestLoginHelper {
return true;
}
static Future<void> enterLoginCredentials(
WidgetTester tester, {
Future<void> enterCredentials({
String server = "",
String email = "",
String password = "",
@ -50,6 +54,70 @@ class ImmichTestLoginHelper {
await tester.pump(const Duration(milliseconds: 500));
await tester.enterText(loginForms.at(2), server);
await tester.pump(const Duration(milliseconds: 500));
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
}
Future<void> enterCredentialsOf(LoginCredentials credentials) async {
await enterCredentials(
server: credentials.server,
email: credentials.email,
password: credentials.password,
);
}
Future<void> pressLoginButton() async {
final button = find.textContaining("login_form_button_text".tr());
await tester.tap(button);
}
Future<void> assertLoginSuccess({int timeoutSeconds = 15}) async {
for (var i = 0; i < timeoutSeconds * 2; i++) {
if (tester.any(find.text("home_page_building_timeline".tr()))) {
return;
}
await tester.pump(const Duration(milliseconds: 500));
}
fail("Login failed.");
}
Future<void> assertLoginFailed({int timeoutSeconds = 15}) async {
for (var i = 0; i < timeoutSeconds * 2; i++) {
if (tester.any(find.text("login_form_failed_login".tr()))) {
return;
}
await tester.pump(const Duration(milliseconds: 500));
}
fail("Timeout.");
}
}
enum LoginCredentials {
testInstance(
"https://flutter-int-test.preview.immich.app",
"demo@immich.app",
"demo",
),
testInstanceButWithWrongPassword(
"https://flutter-int-test.preview.immich.app",
"demo@immich.app",
"wrong",
),
wrongInstanceUrl(
"https://does-not-exist.preview.immich.app",
"demo@immich.app",
"demo",
);
const LoginCredentials(this.server, this.email, this.password);
final String server;
final String email;
final String password;
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -52,7 +53,10 @@ class HomePage extends HookConsumerWidget {
});
return () {
selectionEnabledHook.dispose();
// This does not work in tests
if (kReleaseMode) {
selectionEnabledHook.dispose();
}
};
},
[],
@ -162,28 +166,28 @@ class HomePage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
'Building the timeline',
'home_page_building_timeline',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Theme.of(context).primaryColor,
),
),
).tr(),
),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: tipOneOpacity.value,
child: const SizedBox(
child: SizedBox(
width: 250,
child: Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
'If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).',
padding: const EdgeInsets.only(top: 8.0),
child: const Text(
'home_page_first_time_notice',
textAlign: TextAlign.justify,
style: TextStyle(
fontSize: 12,
),
),
).tr(),
),
),
)