From ddf81eeec059110ee9dc4b1ed57f9924b30dffe5 Mon Sep 17 00:00:00 2001 From: Bhasher Date: Fri, 16 Jun 2023 10:42:10 +0200 Subject: [PATCH] Widget tests & auto github update --- .githooks/pre-commit | 19 ++++++- lib/main.dart | 57 +++++++++++++------ lib/models/app_data.dart | 10 ++++ lib/utils/ext/datetime.dart | 1 - lib/utils/helper/release_version.dart | 48 ++++++++++++++++ lib/utils/helper/text_input_formatter.dart | 1 - lib/utils/helper/update_box.dart | 48 ++++++++++++++++ pubspec.lock | 54 +++++++++++++++++- pubspec.yaml | 5 +- test/widgets/header_tile_test.dart | 22 ++++++++ test/widgets/new_screen_test.dart | 56 ++++++++++++++++++ test/widgets/text_switch_test.dart | 66 ++++++++++++++++++++++ 12 files changed, 362 insertions(+), 25 deletions(-) create mode 100644 lib/utils/helper/release_version.dart create mode 100644 lib/utils/helper/update_box.dart create mode 100644 test/widgets/header_tile_test.dart create mode 100644 test/widgets/new_screen_test.dart create mode 100644 test/widgets/text_switch_test.dart diff --git a/.githooks/pre-commit b/.githooks/pre-commit index c1c2b45..06b7982 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,2 +1,19 @@ +#!/bin/sh + +set -e + +cleanup() { + git stash pop --index +} + +trap cleanup EXIT + +git stash push --keep-index + +dart format . + flutter analyze -flutter test \ No newline at end of file + +flutter test + +exit 0 diff --git a/lib/main.dart b/lib/main.dart index e292c91..fa58001 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,19 +1,18 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; +import 'package:splitr/utils/helper/release_version.dart'; +import 'package:tuple/tuple.dart'; import 'screens/project/project_page.dart'; import 'screens/projects_list/projects_list_page.dart'; import 'models/app_data.dart'; import 'screens/new_project/new_project.dart'; import 'utils/helper/theme.dart'; +import 'utils/helper/update_box.dart'; void main() async { runApp(const SplashScreen()); await AppData.init(); - if (AppData.firstRun) { - runApp(const SetupScreen()); - } else { - runApp(const MainScreen()); - } + runApp(const MainScreen()); } class SplashScreen extends StatelessWidget { @@ -46,27 +45,49 @@ class MainScreen extends StatelessWidget { theme: defaultTheme, darkTheme: defaultDarkTheme, themeMode: ThemeMode.system, - home: AppData.current == null - ? const ProjectsListPage() - : ProjectPage(AppData.current!), + home: const CheckUpdatePage(), ); }); } } -class SetupScreen extends StatelessWidget { - const SetupScreen({super.key}); +class CheckUpdatePage extends StatelessWidget { + const CheckUpdatePage({super.key}); @override Widget build(BuildContext context) { - return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) { - return MaterialApp( - title: 'Splitr', - theme: defaultTheme, - darkTheme: defaultDarkTheme, - themeMode: ThemeMode.system, - home: NewProjectScreen(first: true), - ); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + try { + Tuple3 hasNewRelease = await checkForNewRelease(); + + if (hasNewRelease.item1) { + if (context.mounted) { + updateBox( + context: context, + currentVersion: + AppData.sharedPreferences.getString('last_version') ?? 'null', + releaseVersion: hasNewRelease.item2, + releaseUrl: hasNewRelease.item3, + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error when trying to fetch releases data'), + ), + ); + } + } }); + + if (AppData.firstRun) { + return NewProjectScreen(first: true); + } else if (AppData.current == null) { + return const ProjectsListPage(); + } else { + return ProjectPage(AppData.current!); + } } } diff --git a/lib/models/app_data.dart b/lib/models/app_data.dart index 53af673..201df44 100644 --- a/lib/models/app_data.dart +++ b/lib/models/app_data.dart @@ -1,9 +1,11 @@ import 'package:app_links/app_links.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:splitr/data/local/project.dart'; import 'package:splitr/utils/ext/set.dart'; +import 'package:splitr/utils/helper/release_version.dart'; import 'package:sqflite/sqflite.dart'; import '../screens/new_project/new_project.dart'; @@ -49,6 +51,7 @@ class AppData { static init() async { hasBeenInit = true; sharedPreferences = await SharedPreferences.getInstance(); + db = await SplitrDatabase.instance.database; AppData.instances = await Instance.getAllInstances(); @@ -85,6 +88,13 @@ class AppData { ); } }); + + String currentVersion = (await PackageInfo.fromPlatform()).version; + + if (!sharedPreferences.containsKey('last_version') || + isNewer(sharedPreferences.getString('last_version')!, currentVersion)) { + await sharedPreferences.setString('last_version', currentVersion); + } } } diff --git a/lib/utils/ext/datetime.dart b/lib/utils/ext/datetime.dart index a25c82a..573f3e1 100644 --- a/lib/utils/ext/datetime.dart +++ b/lib/utils/ext/datetime.dart @@ -1,4 +1,3 @@ - extension DateTimeExtension on DateTime { bool operator <(DateTime other) { return millisecondsSinceEpoch < other.millisecondsSinceEpoch; diff --git a/lib/utils/helper/release_version.dart b/lib/utils/helper/release_version.dart new file mode 100644 index 0000000..970b528 --- /dev/null +++ b/lib/utils/helper/release_version.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:splitr/models/app_data.dart'; +import 'package:tuple/tuple.dart'; + +String repository = + 'https://api.github.com/repos/bhasherbel/splitr/releases/latest'; + +Future> checkForNewRelease() async { + final response = await http.get(Uri.parse(repository)); + + if (response.statusCode != 200) { + return const Tuple3(false, '', ''); + } + + final releaseData = json.decode(response.body); + + String releaseVersion = releaseData['tag_name'] as String; + + String lastChecked = AppData.sharedPreferences.getString('last_version') ?? + (await PackageInfo.fromPlatform()).version; + + if (isNewer(lastChecked, releaseVersion)) { + String url = releaseData['html_url'] as String; + + return Tuple3(true, releaseVersion, url); + } + + return const Tuple3(false, '', ''); +} + +bool isNewer(String old, String new_) { + final parts1 = old.split('+')[0].split('.'); + final parts2 = new_.split('+')[0].split('.'); + + for (int i = 0; i < parts1.length; i++) { + final int part1 = int.parse(parts1[i]); + final int part2 = int.parse(parts2[i]); + + if (part1 != part2) { + return part2 > part1; + } + } + + return false; +} diff --git a/lib/utils/helper/text_input_formatter.dart b/lib/utils/helper/text_input_formatter.dart index 39be1fc..73fea08 100644 --- a/lib/utils/helper/text_input_formatter.dart +++ b/lib/utils/helper/text_input_formatter.dart @@ -1,4 +1,3 @@ - import 'dart:math'; import 'package:flutter/services.dart'; diff --git a/lib/utils/helper/update_box.dart b/lib/utils/helper/update_box.dart new file mode 100644 index 0000000..3877983 --- /dev/null +++ b/lib/utils/helper/update_box.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:splitr/models/app_data.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +Future updateBox({ + required BuildContext context, + required String currentVersion, + required String releaseVersion, + required String releaseUrl, +}) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('New Update Available'), + content: + Text('Update from $currentVersion to $releaseVersion is available'), + actions: [ + ButtonBar( + alignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + AppData.sharedPreferences + .setString('last_version', releaseVersion); + Navigator.of(context).pop(); + }, + child: const Text('Skip'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Later'), + ), + TextButton( + onPressed: () async { + await launchUrlString( + releaseUrl, + mode: LaunchMode.externalApplication, + ); + if (context.mounted) Navigator.of(context).pop(); + }, + child: const Text('Update'), + ), + ], + ) + ], + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index eb89ab8..5d19fb4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,13 +241,13 @@ packages: source: hosted version: "2.1.2" http: - dependency: transitive + dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_multi_server: dependency: transitive description: @@ -376,6 +376,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7cb1947cbb7e707edce15641730cf16ddf8e545b281132da8d60538885609eeb" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: "direct main" description: @@ -733,6 +749,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + url: "https://pub.dev" + source: hosted + version: "6.1.11" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 + url: "https://pub.dev" + source: hosted + version: "6.0.35" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" url_launcher_linux: dependency: transitive description: @@ -741,6 +781,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.4" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" + source: hosted + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3904c25..6f7a6ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: splitr is a free and open-source app that lets you create multiple publish_to: 'none' -version: 0.4.2 +version: 0.4.3 environment: sdk: '>=2.18.6 <3.0.0' @@ -26,6 +26,9 @@ dependencies: share_plus: ^6.3.1 flutter_speed_dial: ^6.2.0 app_links: ^3.4.3 + package_info_plus: ^3.0.0 + http: ^0.13.6 + url_launcher: ^6.1.11 dev_dependencies: flutter_test: diff --git a/test/widgets/header_tile_test.dart b/test/widgets/header_tile_test.dart new file mode 100644 index 0000000..f489d92 --- /dev/null +++ b/test/widgets/header_tile_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/widgets/header_tile.dart'; + +void main() { + testWidgets('HeaderTile should render correctly with the provided text', + (WidgetTester tester) async { + const text = 'Header Title'; + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: HeaderTile( + text, + smallCaps: false, + ), + ), + ), + ); + + expect(find.text(text), findsOneWidget); + }); +} diff --git a/test/widgets/new_screen_test.dart b/test/widgets/new_screen_test.dart new file mode 100644 index 0000000..6680dc4 --- /dev/null +++ b/test/widgets/new_screen_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/widgets/new_screen.dart'; + +void main() { + group('NewScreen Widget', () { + Future pumpNewScreen(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return NewScreen( + child: Container(), + ); + }, + ), + ), + ); + } + + testWidgets('NewScreen should display child widget', + (WidgetTester tester) async { + await pumpNewScreen(tester); + + expect(find.byType(Container), findsOneWidget); + }); + + testWidgets('NewScreen should call onValidate when button is pressed', + (WidgetTester tester) async { + bool onValidateCalled = false; + + await pumpNewScreen(tester); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return NewScreen( + child: Container(), + onValidate: (context, formKey) { + onValidateCalled = true; + return Future.value(); + }, + ); + }, + ), + ), + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect(onValidateCalled, isTrue); + }); + }); +} diff --git a/test/widgets/text_switch_test.dart b/test/widgets/text_switch_test.dart new file mode 100644 index 0000000..fb4ba5e --- /dev/null +++ b/test/widgets/text_switch_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/widgets/text_switch.dart'; + +void main() { + group('TexteSwitch', () { + testWidgets( + 'TextSwitch should render correctly with the provided texts and initial state', + (WidgetTester tester) async { + const leftText = 'Left'; + const rightText = 'Right'; + const initialState = false; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: TextSwitch( + state: initialState, + leftText: leftText, + rightText: rightText, + ), + ), + ), + ); + + expect(find.text(leftText), findsOneWidget); + expect(find.text(rightText), findsOneWidget); + + final switchFinder = find.byType(Switch); + expect(switchFinder, findsOneWidget); + + final switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, initialState); + }); + + testWidgets( + 'TextSwitch should call onChanged callback when the switch state is changed', + (WidgetTester tester) async { + bool onChangedCalled = false; + bool newState = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TextSwitch( + state: false, + onChanged: (bool state) { + onChangedCalled = true; + newState = state; + }, + ), + ), + ), + ); + + final switchFinder = find.byType(Switch); + expect(switchFinder, findsOneWidget); + + await tester.tap(switchFinder); + await tester.pumpAndSettle(); + + expect(onChangedCalled, isTrue); + expect(newState, isTrue); + }); + }); +}