diff --git a/.gitignore b/.gitignore index 46565e3..136b493 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ app.*.map.json .fvm/flutter_sdk _nocodb + +integration_test/.env diff --git a/Makefile b/Makefile index f15112b..2157f72 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -fix: - dart fix --apply lib +run_integration_test rit: + flutter run --dart-define-from-file=integration_test/.env -t integration_test/hello_test.dart fmt: - dart format lib + dart fix --apply lib + dart fix --apply integration_test + dart format lib integration_test build-runner-watch watch: # flutter pub run build_runner build -d -v @@ -25,7 +27,6 @@ enable-db-log: tail-db-log: cd _nocodb/docker-compose/pg && docker-compose logs -f root_db - remove-generated-files: find . | grep -e 'freezed\.dart' -e '\.g\.dart' | xargs -I {} rm {} @@ -36,4 +37,4 @@ cloc: cloc . --vcs=git --include-ext=dart,yaml lib run-web: - CHROME_EXECUTABLE="./scripts/google-chrome-unsafe.sh" flutter run -d chrome \ No newline at end of file + CHROME_EXECUTABLE="./scripts/google-chrome-unsafe.sh" flutter run -d chrome diff --git a/android/app/build.gradle b/android/app/build.gradle index b879b53..5441b8e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,6 +48,14 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + + // NOTE: To fix the following error. + // + // ERROR:D8: Cannot fit requested classes in a single dex file (# methods: 81117 > 65536) + // com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: + // The number of method references in a .dex file cannot exceed 64K. + // Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html + multiDexEnabled true } buildTypes { diff --git a/integration_test/hello_test.dart b/integration_test/hello_test.dart new file mode 100644 index 0000000..795fb4c --- /dev/null +++ b/integration_test/hello_test.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:nocodb/main.dart'; + +const NC_ENDPOINT = String.fromEnvironment('NC_ENDPOINT'); +const NC_USER = String.fromEnvironment('NC_USER'); +const NC_PASS = String.fromEnvironment('NC_PASS'); + +// https://github.com/flutter/flutter/issues/88765#issuecomment-1113140289 +Future waitFor( + final WidgetTester tester, + final Finder finder, { + final Duration timeout = const Duration(seconds: 20), +}) async { + final end = DateTime.now().add(timeout); + + do { + if (DateTime.now().isAfter(end)) { + throw Exception('Timed out waiting for $finder'); + } + + await tester.pumpAndSettle(); + await Future.delayed(const Duration(milliseconds: 100)); + } while (finder.evaluate().isEmpty); +} + +void main() { + HttpOverrides.global = null; + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('sign in', (final WidgetTester tester) async { + await tester.pumpWidget( + const ProviderScope( + child: App(), + ), + ); + await waitFor(tester, find.text('SIGN IN')); + + await tester.enterText( + find.byKey(const ValueKey('email')), + NC_USER, + ); + await tester.enterText( + find.byKey(const ValueKey('password')), + NC_PASS, + ); + await tester.enterText( + find.byKey(const ValueKey('endpoint')), + NC_ENDPOINT, + ); + await tester.tap( + find.byKey(const ValueKey('sign_in_button')), + ); + + await tester.pumpAndSettle(); + + await waitFor(tester, find.text('Projects')); + }); +} diff --git a/lib/features/core/components/dialog/fields_dialog.dart b/lib/features/core/components/dialog/fields_dialog.dart index 6983424..7987bf7 100644 --- a/lib/features/core/components/dialog/fields_dialog.dart +++ b/lib/features/core/components/dialog/fields_dialog.dart @@ -156,9 +156,7 @@ class FieldsDialog extends HookConsumerWidget { ), InkWell( onTap: () { - ref - .read(viewProvider.notifier) - .showSystemFields(); + ref.read(viewProvider.notifier).showSystemFields(); }, child: Row( children: [ diff --git a/lib/features/core/components/editor.dart b/lib/features/core/components/editor.dart index 5d01d26..3a322e6 100644 --- a/lib/features/core/components/editor.dart +++ b/lib/features/core/components/editor.dart @@ -96,6 +96,7 @@ class Editor extends HookConsumerWidget { maxLines: null, keyboardType: TextInputType.multiline, ); + case UITypes.links: case UITypes.linkToAnotherRecord: final tables = ref.watch(tablesProvider); final relation = tables?.relationMap[column.fkRelatedModelId]; diff --git a/lib/features/core/components/editors/link_to_another_record.dart b/lib/features/core/components/editors/link_to_another_record.dart index 5695e1e..19d838f 100644 --- a/lib/features/core/components/editors/link_to_another_record.dart +++ b/lib/features/core/components/editors/link_to_another_record.dart @@ -53,7 +53,7 @@ class LinkToAnotherRecord extends HookConsumerWidget { return [ ...records.map((final record) { final (pk, pv) = record; - return _buildCard(value: pv, refRowId: pk, ref: ref); + return _buildCard(value: pv.toString(), refRowId: pk, ref: ref); }), if (9 < records.length) Card( diff --git a/lib/features/core/providers/providers.dart b/lib/features/core/providers/providers.dart index d19d9c1..ef543e0 100644 --- a/lib/features/core/providers/providers.dart +++ b/lib/features/core/providers/providers.dart @@ -60,7 +60,8 @@ class View extends _$View { data: { 'show_system_fields': !state!.showSystemFields, }, - ), serializer: (final ok) => state = ok, + ), + serializer: (final ok) => state = ok, ); } diff --git a/lib/features/sign_in/pages/sign_in.dart b/lib/features/sign_in/pages/sign_in.dart index 6531458..83381bc 100644 --- a/lib/features/sign_in/pages/sign_in.dart +++ b/lib/features/sign_in/pages/sign_in.dart @@ -62,6 +62,7 @@ class SignInPage extends HookConsumerWidget { child: Column( children: [ TextField( + key: const ValueKey('email'), autofillHints: const [ AutofillHints.email, AutofillHints.username, @@ -72,6 +73,7 @@ class SignInPage extends HookConsumerWidget { ), ), TextField( + key: const ValueKey('password'), autofillHints: const [ AutofillHints.password, ], @@ -92,9 +94,10 @@ class SignInPage extends HookConsumerWidget { obscureText: !showPassword.value, ), TextField( + key: const ValueKey('endpoint'), onChanged: (final value) { EasyDebounce.debounce( - 'sign_in_password', + 'api_endpoint', const Duration(seconds: 1), () async { await api @@ -138,6 +141,7 @@ class SignInPage extends HookConsumerWidget { ), actions: [ TextButton( + key: const ValueKey('sign_in_button'), child: const Text('SIGN IN'), onPressed: () async { api.init(apiUrlController.text); @@ -147,7 +151,6 @@ class SignInPage extends HookConsumerWidget { passwordController.text, ) .then((final authToken) async { - // TODO: Need to rewrite Settings class. The following values should not be saved to the storage when rememberMe is false. await settings.setApiBaseUrl(apiUrlController.text); await authToken.when( ok: (final token) async { @@ -155,6 +158,8 @@ class SignInPage extends HookConsumerWidget { await settings.setEmail(emailController.text); await settings.setRememberMe(rememberMe.value); await settings.setAuthToken(token); + } else { + await settings.clear(); } if (context.mounted) { const ProjectListRoute().go(context); diff --git a/lib/main.dart b/lib/main.dart index da55535..c233ca0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,7 +30,6 @@ void main() async { return stack; }; - // WidgetsFlutterBinding.ensureInitialized(); runApp( const ProviderScope( child: App(), diff --git a/lib/nocodb_sdk/client.dart b/lib/nocodb_sdk/client.dart index 172cd33..0853cb2 100644 --- a/lib/nocodb_sdk/client.dart +++ b/lib/nocodb_sdk/client.dart @@ -83,7 +83,6 @@ class _Api { init(final String url, {final String? authToken}) { _baseUri = Uri.parse(url); - logger.info(_baseUri); if (authToken != null) { _client.addHeaders({'xc-auth': authToken}); } else { @@ -243,16 +242,13 @@ class _Api { }, ); - // Future> me( - // [final Map? queryParameters]) async => - // await _send2( - // method: HttpMethod.get, - // path: '/api/v1/auth/user/me', - // serializer: (final _, final data) { - // final user = model.NcUser.fromJson(data); - // return Result.ok(user); - // }, - // ); + Future> authUserMe( + [final Map? queryParameters]) async => + await _send( + method: HttpMethod.get, + path: '/api/v1/auth/user/me', + serializer: (final _, final data) => model.NcUser.fromJson(data), + ); Future>> projectList() async => await _send( diff --git a/lib/router.dart b/lib/router.dart index 5c22ca3..2afdb0d 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -8,6 +8,7 @@ import 'package:nocodb/common/preferences.dart'; import 'package:nocodb/common/settings.dart'; import 'package:nocodb/nocodb_sdk/client.dart'; import 'package:nocodb/routes.dart'; +import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'router.g.dart'; @@ -21,8 +22,8 @@ part 'router.g.dart'; final rawHeader = parts[0]; final rawPayload = parts[1]; - final header = String.fromCharCodes(base64Decode(rawHeader)); - final payload = String.fromCharCodes(base64Decode(rawPayload)); + final header = String.fromCharCodes(base64Decode(base64.normalize(rawHeader))); + final payload = String.fromCharCodes(base64Decode(base64.normalize(rawPayload))); return (jsonDecode(header), jsonDecode(payload)); } @@ -62,14 +63,17 @@ FutureOr redirect( } final rememberMe = await settings.rememberMe; + if (!rememberMe) { + return null; + } + final apiBaseUrl = await settings.apiBaseUrl; final authToken = await settings.authToken; logger.config('apiBaseUrl: $apiBaseUrl'); - if (authToken == null || apiBaseUrl == null) { return const HomeRoute().location; - } else if (state.path == const HomeRoute().location) { + } else if (state.path == null || state.path == const HomeRoute().location) { if (!rememberMe) { await settings.clear(); return const HomeRoute().location; @@ -81,24 +85,22 @@ FutureOr redirect( ); if (isAlive) { - final result = await api.version(apiBaseUrl); - result.when( - ok: (final ok) { - if (!ok) { - if (!ok) { - return const HomeRoute().location; - } - api.init(apiBaseUrl, authToken: authToken); - return const ProjectListRoute().location; - } - }, - ng: (final error, final stackTrace) => - notifyError(context, error, stackTrace), - ); + api.init(apiBaseUrl, authToken: authToken); + return const ProjectListRoute().location; + // final result = await api.authUserMe(); + // return result.when( + // ok: (final ok) { + // api.init(apiBaseUrl, authToken: authToken); + // return const ProjectListRoute().location; + // }, + // ng: (final error, final stackTrace) => + // notifyError(context, error, stackTrace), + // ); } } - } catch (e) { - logger.warning(e); + } catch (e, s) { + logger..warning(e) + ..warning(s); return const HomeRoute().location; } @@ -110,9 +112,8 @@ GoRouter router(final RouterRef ref) => GoRouter( routes: $appRoutes, debugLogDiagnostics: true, redirect: (final context, final state) async { - logger.info('redirecting ...'); final location = await redirect(context, state); - if (location == null) { + if (location != null) { logger.info('redirected to $location'); } return location; diff --git a/pubspec.lock b/pubspec.lock index a9599b2..4f3ee85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -350,6 +350,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.7" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_expandable_fab: dependency: "direct main" description: @@ -472,6 +477,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -600,6 +610,11 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -796,10 +811,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -824,6 +839,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" pub_semver: dependency: transitive description: @@ -1053,6 +1076,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" synchronized: dependency: transitive description: @@ -1141,6 +1172,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7c574c..f0c0b60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: http_parser: ^4.0.2 dev_dependencies: + integration_test: + sdk: flutter flutter_test: sdk: flutter flutter_lints: diff --git a/scripts/run_android_emulator.sh b/scripts/run_android_emulator.sh new file mode 100755 index 0000000..a5b13f8 --- /dev/null +++ b/scripts/run_android_emulator.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +cd ~/Library/Android/sdk/emulator +./emulator -avd $(emulator -list-avds | head -n 1) & +