You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-08 23:07:06 +02:00
refactor(mobile): maplibre (#6087)
* chore: maplibre gl pubspec * refactor(wip): maplibre for maps * refactor(wip): dual pane + location button * chore: remove flutter_map and deps * refactor(wip): map zoom to location * refactor: location picker * open gallery_viewer on marker tap * remove detectScaleGesture param * test: debounce and throttle * chore: rename get location method * feat(mobile): Adds gps locator to map prompt for easy geolocation (#6282) * Refactored get gps coords * Use var for linter's sake, should handle errors better * Cleanup * Fix linter issues * chore(dep): update maplibre to official lib --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Joshua Herrera <joshua.herrera227@gmail.com>
This commit is contained in:
@ -203,7 +203,7 @@ void main() {
|
||||
late ProviderContainer container;
|
||||
|
||||
setUp(() async {
|
||||
settingsMock = AppSettingsServiceMock();
|
||||
settingsMock = MockAppSettingsService();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [
|
||||
appSettingsServiceProvider.overrideWith((ref) => settingsMock),
|
||||
@ -283,7 +283,7 @@ void main() {
|
||||
late ProviderContainer container;
|
||||
|
||||
setUp(() async {
|
||||
settingsMock = AppSettingsServiceMock();
|
||||
settingsMock = MockAppSettingsService();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [
|
||||
appSettingsServiceProvider.overrideWith((ref) => settingsMock),
|
||||
|
18
mobile/test/modules/map/map_mocks.dart
Normal file
18
mobile/test/modules/map/map_mocks.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/map/models/map_state.model.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockMapStateNotifier extends Notifier<MapState>
|
||||
with Mock
|
||||
implements MapStateNotifier {
|
||||
final MapState initState;
|
||||
|
||||
MockMapStateNotifier(this.initState);
|
||||
|
||||
@override
|
||||
MapState build() => initState;
|
||||
|
||||
@override
|
||||
set state(MapState mapState) => super.state = mapState;
|
||||
}
|
165
mobile/test/modules/map/map_theme_override_test.dart
Normal file
165
mobile/test/modules/map/map_theme_override_test.dart
Normal file
@ -0,0 +1,165 @@
|
||||
@Tags(['widget'])
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/map/models/map_state.model.dart';
|
||||
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
|
||||
import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
import '../../widget_tester_extensions.dart';
|
||||
import 'map_mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockMapStateNotifier mapStateNotifier;
|
||||
late List<Override> overrides;
|
||||
late MapState mapState;
|
||||
|
||||
setUpAll(() async {
|
||||
TestUtils.init();
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mapState = MapState(themeMode: ThemeMode.dark);
|
||||
mapStateNotifier = MockMapStateNotifier(mapState);
|
||||
overrides = [mapStateNotifierProvider.overrideWith(() => mapStateNotifier)];
|
||||
});
|
||||
|
||||
testWidgets("Return dark theme style when theme mode is dark",
|
||||
(tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOveride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
mapStateNotifier.state =
|
||||
mapState.copyWith(darkStyleFetched: const AsyncData("dark"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "dark");
|
||||
});
|
||||
|
||||
testWidgets("Return error when style is not fetched", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOveride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
darkStyleFetched: const AsyncError("Error", StackTrace.empty),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.hasError, isTrue);
|
||||
});
|
||||
|
||||
testWidgets("Return light theme style when theme mode is light",
|
||||
(tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOveride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.light,
|
||||
lightStyleFetched: const AsyncData("light"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "light");
|
||||
});
|
||||
|
||||
group("System mode", () {
|
||||
testWidgets("Return dark theme style when system is dark", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOveride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue =
|
||||
Brightness.dark;
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.system,
|
||||
darkStyleFetched: const AsyncData("dark"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(mapStyle?.valueOrNull, "dark");
|
||||
});
|
||||
|
||||
testWidgets("Return light theme style when system is light",
|
||||
(tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOveride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue =
|
||||
Brightness.light;
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.system,
|
||||
lightStyleFetched: const AsyncData("light"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(mapStyle?.valueOrNull, "light");
|
||||
});
|
||||
|
||||
testWidgets("Switches style when system brightness changes",
|
||||
(tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOveride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue =
|
||||
Brightness.light;
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.system,
|
||||
lightStyleFetched: const AsyncData("light"),
|
||||
darkStyleFetched: const AsyncData("dark"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "light");
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue =
|
||||
Brightness.dark;
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "dark");
|
||||
});
|
||||
});
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class AppSettingsServiceMock extends Mock implements AppSettingsService {}
|
||||
class MockAppSettingsService extends Mock implements AppSettingsService {}
|
||||
|
41
mobile/test/modules/utils/debouncer_test.dart
Normal file
41
mobile/test/modules/utils/debouncer_test.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
|
||||
class _Counter {
|
||||
int _count = 0;
|
||||
_Counter();
|
||||
|
||||
int get count => _count;
|
||||
void increment() => _count = _count + 1;
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Executes the method after the interval', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 300));
|
||||
debouncer.run(() => counter.increment());
|
||||
expect(counter.count, 0);
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Executes the method immediately if zero interval', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 0));
|
||||
debouncer.run(() => counter.increment());
|
||||
// Even though it is supposed to be executed immediately, it is added to the async queue and so
|
||||
// we need this delay to make sure the actual debounced method is called
|
||||
await Future.delayed(const Duration(milliseconds: 0));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Delayes method execution after all the calls are completed', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 100));
|
||||
debouncer.run(() => counter.increment());
|
||||
debouncer.run(() => counter.increment());
|
||||
debouncer.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
}
|
47
mobile/test/modules/utils/throttler_test.dart
Normal file
47
mobile/test/modules/utils/throttler_test.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/throttle.dart';
|
||||
|
||||
class _Counter {
|
||||
int _count = 0;
|
||||
_Counter();
|
||||
|
||||
int get count => _count;
|
||||
void increment() {
|
||||
debugPrint("Counter inside increment: $count");
|
||||
_count = _count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Executes the method immediately if no calls received previously',
|
||||
() async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 300));
|
||||
throttler.run(() => counter.increment());
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Does not execute calls before throttle interval', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 100));
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Executes the method if received in intervals', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 100));
|
||||
for (final _ in Iterable<int>.generate(10)) {
|
||||
throttler.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
expect(counter.count, 5);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user