diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 000000000..3ca65ffc7 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.32.8" +} \ No newline at end of file diff --git a/.github/browserstack-devices.yml b/.github/browserstack-devices.yml index 6133e7058..98b14892f 100644 --- a/.github/browserstack-devices.yml +++ b/.github/browserstack-devices.yml @@ -38,7 +38,7 @@ flutter: - "Google Pixel 6-12.0" # Android 12 support ios: devices: - - "iPhone 13-15" # iOS 15 support + - "iPhone 13-16" # iOS 16 support (min deployment target is 15.6) - "iPhone 14-16" # iOS 16 current - "iPhone 12-17" # iOS 17 latest diff --git a/.gitignore b/.gitignore index 9bff26155..4e5401a23 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ packages/ Pods/ target/ xcuserdata + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/flutter_app/README.md b/flutter_app/README.md index 082ebc4cf..6aaed8fe0 100644 --- a/flutter_app/README.md +++ b/flutter_app/README.md @@ -7,14 +7,25 @@ This is a basic task application that demonstrates how to use Ditto's peer-to-pe ## Prerequisites - Dart SDK installed -- Flutter SDK installed (tested on 3.24) -- Java Virtual Machine (JVM) 11 installed +- Flutter SDK installed (tested on 3.29/3.32) +- Java Virtual Machine (JVM) 11 or greater installed - Git command line installed (Windows requirement) - XCode installed (for iOS development) - Android Studio installed (for managing the Android SDK) - Android SDK installed - IDE of choice (Android Studio, VS Code, etc) +### Flutter Version Manager +If you need to test on multiple versions of Flutter, it's recommended to use `fvm` (Flutter Version Manager). You can find documentation [here](https://fvm.app/documentation/getting-started/overview). + +### MacOS Development +This has been tested with XCode 26.2 on MacOS 26.6 with Flutter 3.29.3 && 3.32.8. + +### Windows Development +To build the Windows version of this Flutter app requires Visual Studio 2022 specifically be +installed and configured with C++ and CMake installed from the Visual Studio Installer. This has +been tested with Flutter version 3.29.3 on Windows. + ## Getting Started ### 1. Clone the Repository @@ -78,7 +89,7 @@ Please choose one (or "q" to quit): > If you are going to use a physical iPhone, you will need to update the Team under Signing & Capabilities in XCode. You can open the ios/Runner.xcodeproj file in XCode and then set your team from the Runner Target -> Signing & Capabilities tab. > -- Ensure that cocoapods is up to date +- Ensure that cocoapods is up to date (or you can use Homebrew with `brew install cocoapods`) ```bash gem install cocoapods diff --git a/flutter_app/integration_test/app_test.dart b/flutter_app/integration_test/app_test.dart index ae42476ad..4adacedf6 100644 --- a/flutter_app/integration_test/app_test.dart +++ b/flutter_app/integration_test/app_test.dart @@ -17,21 +17,15 @@ void main() { (WidgetTester tester) async { // Initialize app await app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); + // Allow up to 10 seconds for Ditto to initialise and the first sync + // exchange to complete. pumpAndSettle returns as soon as the UI is + // idle, so on fast devices this resolves much sooner. + await tester.pumpAndSettle(const Duration(seconds: 10)); - // Tap "OK" button if Bluetooth permission dialog appears (iOS) - final okButton = find.text('OK'); - if (okButton.evaluate().isNotEmpty) { - await tester.tap(okButton); - await tester.pumpAndSettle(const Duration(seconds: 2)); - } - - // Tap "Allow" button if local network permission dialog appears (iOS) - final allowButton = find.text('Allow'); - if (allowButton.evaluate().isNotEmpty) { - await tester.tap(allowButton); - await tester.pumpAndSettle(const Duration(seconds: 2)); - } + // NOTE: iOS system permission dialogs (Bluetooth, Local Network) are + // native UIAlertControllers and cannot be found or tapped via Flutter's + // widget-tree finders. They are handled at the XCTest layer in + // ios/RunnerTests/RunnerTests.m via addUIInterruptionMonitor. // Verify app title is present expect(find.text('Ditto Tasks'), findsOneWidget); @@ -45,11 +39,9 @@ void main() { // Verify clear button is present expect(find.byIcon(Icons.clear), findsOneWidget); - // Wait for sync to complete - await Future.delayed(const Duration(seconds: 5)); - await tester.pumpAndSettle(); - - // Look for the test document that should be synced from Ditto cloud + // Look for the test document that should be synced from Ditto cloud. + // The playground can accumulate many documents from previous CI runs, + // so we poll rather than waiting a fixed amount of time. const testTitle = String.fromEnvironment('TASK_TO_FIND'); if (testTitle.isEmpty) { @@ -57,9 +49,23 @@ void main() { 'Build with: --dart-define=TASK_TO_FIND='); } - expect(find.text(testTitle), findsOneWidget, + // Poll every 500 ms for up to 45 seconds to give the cloud sync + // enough time to deliver and write all documents to the local store. + const syncTimeout = Duration(seconds: 45); + final deadline = DateTime.now().add(syncTimeout); + bool taskFound = false; + + while (DateTime.now().isBefore(deadline)) { + await tester.pump(const Duration(milliseconds: 500)); + if (find.text(testTitle).evaluate().isNotEmpty) { + taskFound = true; + break; + } + } + + expect(taskFound, isTrue, reason: - 'Should find test document with title: $testTitle synced from Ditto cloud'); + 'Should find test document with title: $testTitle synced from Ditto cloud within ${syncTimeout.inSeconds}s'); }); }); } diff --git a/flutter_app/ios/Podfile.lock b/flutter_app/ios/Podfile.lock index 1757271fa..cd68bce69 100644 --- a/flutter_app/ios/Podfile.lock +++ b/flutter_app/ios/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (4.14.3): - - DittoFlutter (= 4.14.3) + - ditto_live (5.0.0): + - DittoFlutter (= 5.0.0) - Flutter - - DittoFlutter (4.14.3) + - DittoFlutter (5.0.0) - Flutter (1.0.0) - integration_test (0.0.1): - Flutter @@ -36,9 +36,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: - ditto_live: 01cde75b8b98f45b65c13dfad5b4667e1bcf0306 - DittoFlutter: 1745a5b3152fb758f1604f5f9a26fb3d64362dba - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + ditto_live: f038d915f2ad9206d180d3fd8b231d149b71e3f2 + DittoFlutter: 90c4bcdf128b0a14bf8f6f923369e7a6ddccd27d + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d diff --git a/flutter_app/ios/Runner.xcodeproj/project.pbxproj b/flutter_app/ios/Runner.xcodeproj/project.pbxproj index 8ebdad35c..1345e7d89 100644 --- a/flutter_app/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter_app/ios/Runner.xcodeproj/project.pbxproj @@ -494,6 +494,7 @@ DEVELOPMENT_TEAM = 3T2VMFZPPQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -685,6 +686,7 @@ DEVELOPMENT_TEAM = 3T2VMFZPPQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -710,6 +712,7 @@ DEVELOPMENT_TEAM = 3T2VMFZPPQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1728d1042..e3773d42e 100644 --- a/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Uses WiFi to connect and sync with nearby devices. NSBonjourServices - _http-alt._tcp. + _http-alt._tcp. + + + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral diff --git a/flutter_app/ios/RunnerTests/RunnerTests.m b/flutter_app/ios/RunnerTests/RunnerTests.m index 73977793b..37c04433a 100644 --- a/flutter_app/ios/RunnerTests/RunnerTests.m +++ b/flutter_app/ios/RunnerTests/RunnerTests.m @@ -1,3 +1,46 @@ @import XCTest; - @import integration_test; - INTEGRATION_TEST_IOS_RUNNER(RunnerTests) +@import integration_test; + +// This file replaces the one-liner INTEGRATION_TEST_IOS_RUNNER macro so we +// can add a UIInterruptionMonitor. Without it, native iOS system permission +// dialogs (Bluetooth, Local Network, etc.) block the test and can never be +// dismissed by Flutter's find.text() which only searches Flutter widgets. +// +// The interruption monitor fires whenever a springboard-level alert appears +// during the test and automatically taps the first "accept" button it finds. + +@interface RunnerTests : XCTestCase +@end + +@implementation RunnerTests + ++ (XCTestSuite *)defaultTestSuite { + return [IntegrationTestIosRunner defaultTestSuiteForIntegrationTestRunner:self]; +} + +- (void)setUp { + [super setUp]; + + // Automatically accept system permission dialogs so that Ditto's + // Bluetooth and Local Network transports can initialise during tests. + // "OK" – Bluetooth usage alert + // "Allow" / "Allow While Using App" – Local Network usage alert + [self addUIInterruptionMonitorWithDescription:@"System Permission Alert" + handler:^BOOL(XCUIElement *alert) { + NSArray *acceptLabels = @[ + @"OK", + @"Allow", + @"Allow While Using App" + ]; + for (NSString *label in acceptLabels) { + XCUIElement *button = alert.buttons[label]; + if (button.exists) { + [button tap]; + return YES; + } + } + return NO; + }]; +} + +@end diff --git a/flutter_app/lib/dialog.dart b/flutter_app/lib/dialog.dart index acb7447b4..09ba560ae 100644 --- a/flutter_app/lib/dialog.dart +++ b/flutter_app/lib/dialog.dart @@ -20,6 +20,12 @@ class _DialogState extends State<_Dialog> { late final _name = TextEditingController(text: widget.taskToEdit?.title); late var _done = widget.taskToEdit?.done ?? false; + @override + void dispose() { + _name.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) => AlertDialog( icon: const Icon(Icons.add_task), diff --git a/flutter_app/lib/main.dart b/flutter_app/lib/main.dart index 4f2b1b8dd..e1ec06f22 100644 --- a/flutter_app/lib/main.dart +++ b/flutter_app/lib/main.dart @@ -29,9 +29,8 @@ class _DittoExampleState extends State { dotenv.env['DITTO_APP_ID'] ?? (throw Exception("env not found")); final token = dotenv.env['DITTO_PLAYGROUND_TOKEN'] ?? (throw Exception("env not found")); - final authUrl = dotenv.env['DITTO_AUTH_URL']; - final websocketUrl = - dotenv.env['DITTO_WEBSOCKET_URL'] ?? (throw Exception("env not found")); + final authUrl = + dotenv.env['DITTO_AUTH_URL'] ?? (throw Exception("env not found")); @override void initState() { @@ -70,26 +69,31 @@ class _DittoExampleState extends State { await Ditto.init(); - final identity = OnlinePlaygroundIdentity( - appID: appID, - token: token, - enableDittoCloudSync: - false, // This is required to be set to false to use the correct URLs - customAuthUrl: authUrl); - - final ditto = await Ditto.open(identity: identity); - - ditto.updateTransportConfig((config) { - // Note: this will not enable peer-to-peer sync on the web platform - config.setAllPeerToPeerEnabled(true); - config.connect.webSocketUrls.add(websocketUrl); + DittoLogger.isEnabled = true; + DittoLogger.minimumLogLevel = LogLevel.debug; + + //new configuration - https://docs.ditto.live/sdk/latest/ditto-config + final config = DittoConfig( + databaseID: appID, connect: DittoConfigConnectServer(url: authUrl)); + final ditto = await Ditto.open(config); + await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) async { + final authResult = await ditto.auth + .login(token: token, provider: Authenticator.developmentProvider); + if (authResult.exception != null) { + throw authResult.exception!; + } }); - // Disable DQL strict mode - // https://docs.ditto.live/dql/strict-mode - await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false"); + // Register the tasks subscription before starting sync so it is + // included in the very first sync exchange with the cloud. + // Without this, the subscription is registered later when the + // DqlBuilder widget builds, causing a 5–10 second delay on the + // first sync cycle. + ditto.sync.registerSubscription( + "SELECT * FROM tasks WHERE deleted = false", + ); - ditto.startSync(); + ditto.sync.start(); if (mounted) { setState(() => _ditto = ditto); @@ -167,19 +171,22 @@ class _DittoExampleState extends State { Widget get _syncTile => SwitchListTile( title: const Text("Sync Active"), - value: _ditto!.isSyncActive, + value: _ditto!.sync.isActive, onChanged: (value) { if (value) { - setState(() => _ditto!.startSync()); + setState(() => _ditto!.sync.start()); } else { - setState(() => _ditto!.stopSync()); + setState(() => _ditto!.sync.stop()); } }, ); + // Ordering is intentionally omitted here because this screen currently uses + // a single DqlBuilder query, and reintroducing title-based ordering would + // require a different query approach. Widget get _tasksList => DqlBuilder( ditto: _ditto!, - query: "SELECT * FROM tasks WHERE deleted = false ORDER BY title ASC", + query: "SELECT * FROM tasks WHERE deleted = false", builder: (context, result) { final tasks = result.items.map((r) => r.value).map(Task.fromJson); return ListView( @@ -194,7 +201,8 @@ class _DittoExampleState extends State { // Use the Soft-Delete pattern // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern await _ditto!.store.execute( - "UPDATE tasks SET deleted = true WHERE _id = '${task.id}'", + "UPDATE tasks SET deleted = true WHERE _id = :id", + arguments: {"id": task.id}, ); if (mounted) { @@ -209,7 +217,8 @@ class _DittoExampleState extends State { title: Text(task.title), value: task.done, onChanged: (value) => _ditto!.store.execute( - "UPDATE tasks SET done = $value WHERE _id = '${task.id}'", + "UPDATE tasks SET done = :done WHERE _id = :id", + arguments: {"done": value, "id": task.id}, ), secondary: IconButton( icon: const Icon(Icons.edit), @@ -220,7 +229,8 @@ class _DittoExampleState extends State { // https://docs.ditto.live/sdk/latest/crud/update _ditto!.store.execute( - "UPDATE tasks SET title = '${newTask.title}' where _id = '${task.id}'", + "UPDATE tasks SET title = :title WHERE _id = :id", + arguments: {"title": newTask.title, "id": task.id}, ); }, ), diff --git a/flutter_app/macos/Podfile b/flutter_app/macos/Podfile index 29c8eb329..167132a2f 100644 --- a/flutter_app/macos/Podfile +++ b/flutter_app/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/flutter_app/macos/Podfile.lock b/flutter_app/macos/Podfile.lock index 36a676ffa..f1c633448 100644 --- a/flutter_app/macos/Podfile.lock +++ b/flutter_app/macos/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (4.14.3): - - DittoFlutter (= 4.14.3) + - ditto_live (5.0.0): + - DittoFlutter (= 5.0.0) - FlutterMacOS - - DittoFlutter (4.14.3) + - DittoFlutter (5.0.0) - FlutterMacOS (1.0.0) - path_provider_foundation (0.0.1): - Flutter @@ -26,11 +26,11 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin SPEC CHECKSUMS: - ditto_live: ea593266e0fef6ae9e87f94f07bcaf62e331262d - DittoFlutter: 1745a5b3152fb758f1604f5f9a26fb3d64362dba - FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + ditto_live: 38b290325b127be4157b8de4c2105c9d0e1371e9 + DittoFlutter: 90c4bcdf128b0a14bf8f6f923369e7a6ddccd27d + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 -PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 +PODFILE CHECKSUM: 1e95c36afbfd1cb6423ceca4de7a8e1b256fb6ac COCOAPODS: 1.16.2 diff --git a/flutter_app/macos/Runner/DebugProfile.entitlements b/flutter_app/macos/Runner/DebugProfile.entitlements index dddb8a30c..c946719a1 100644 --- a/flutter_app/macos/Runner/DebugProfile.entitlements +++ b/flutter_app/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock index 0a5b45792..4b2ac731f 100644 --- a/flutter_app/pubspec.lock +++ b/flutter_app/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: cbor - sha256: e60380c7329da6b415841be93884b8d4380cbd86cd4cecb2067baa221b8d88b5 + sha256: "2c5c37650f0a2d25149f03e748ab7b2857787bde338f95fe947738b80d713da2" url: "https://pub.dev" source: hosted - version: "6.3.5" + version: "6.5.1" characters: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: "direct main" description: name: ditto_live - sha256: "103c5e3963e1b6988114840ff2ff9d469fb8032124659fb6ec8c8465a19c7c8f" + sha256: "51853b26df7b1de8495bb3bf6a674b1b15b3c60603e80397e0cdb9a66e44457e" url: "https://pub.dev" source: hosted - version: "4.14.3" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" file: dependency: transitive description: @@ -114,10 +114,10 @@ packages: dependency: "direct main" description: name: flutter_dotenv - sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + sha256: d41da11fb497314fbf89811ec30af02d1d898b47980a129f0a8c0a1720460ba2 url: "https://pub.dev" source: hosted - version: "5.2.1" + version: "6.0.1" flutter_driver: dependency: transitive description: flutter @@ -127,10 +127,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -154,14 +154,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - ieee754: - dependency: transitive - description: - name: ieee754 - sha256: "7d87451c164a56c156180d34a4e93779372edd191d2c219206100b976203128c" - url: "https://pub.dev" - source: hosted - version: "1.0.3" integration_test: dependency: "direct dev" description: flutter @@ -203,18 +195,18 @@ packages: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "6.1.0" logger: dependency: transitive description: name: logger - sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999" + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" matcher: dependency: transitive description: @@ -259,18 +251,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.19" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -299,18 +291,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: @@ -477,5 +469,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.9.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index baca97a78..7244f6995 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -20,7 +20,7 @@ version: 1.0.0+1 environment: sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.24.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,15 +32,15 @@ dependencies: flutter: sdk: flutter - ditto_live: ^4.14.3 + ditto_live: 5.0.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 equatable: ^2.0.5 - permission_handler: ^11.3.1 + permission_handler: ^12.0.1 json_annotation: ^4.9.0 - flutter_dotenv: ^5.1.0 + flutter_dotenv: ^6.0.0 dev_dependencies: flutter_test: @@ -53,7 +53,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^4.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/flutter_app/test/widget_test.dart b/flutter_app/test/widget_test.dart index 6b21c2c5d..1b4d96200 100644 --- a/flutter_app/test/widget_test.dart +++ b/flutter_app/test/widget_test.dart @@ -1,43 +1,175 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_quickstart/main.dart'; +import 'package:flutter_quickstart/task.dart'; +import 'package:flutter_quickstart/dialog.dart'; void main() { - setUpAll(() async { - // Initialize dotenv for testing - dotenv.testLoad(fileInput: ''' -DITTO_APP_ID=test_app_id -DITTO_PLAYGROUND_TOKEN=test_playground_token -DITTO_AUTH_URL=https://auth.example.com -DITTO_WEBSOCKET_URL=wss://websocket.example.com -'''); + group('Task model', () { + test('fromJson creates a Task with correct fields', () { + final json = { + '_id': '123', + 'title': 'Buy groceries', + 'done': false, + 'deleted': false, + }; + + final task = Task.fromJson(json); + + expect(task.id, '123'); + expect(task.title, 'Buy groceries'); + expect(task.done, false); + expect(task.deleted, false); + }); + + test('toJson produces correct map', () { + const task = Task( + id: 'abc', + title: 'Walk the dog', + done: true, + deleted: false, + ); + + final json = task.toJson(); + + expect(json['_id'], 'abc'); + expect(json['title'], 'Walk the dog'); + expect(json['done'], true); + expect(json['deleted'], false); + }); + + test('toJson omits null id', () { + const task = Task( + title: 'New task', + done: false, + deleted: false, + ); + + final json = task.toJson(); + + expect(json.containsKey('_id'), false); + }); + + test('fromJson and toJson roundtrip', () { + final original = { + '_id': 'rt-1', + 'title': 'Roundtrip test', + 'done': true, + 'deleted': true, + }; + + final task = Task.fromJson(original); + final result = task.toJson(); + + expect(result['_id'], original['_id']); + expect(result['title'], original['title']); + expect(result['done'], original['done']); + expect(result['deleted'], original['deleted']); + }); }); - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MaterialApp( - home: DittoExample(), - )); + group('Add task dialog', () { + testWidgets('shows Add Task title for new task', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () => showAddTaskDialog(context), + child: const Text('Open'), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(find.text('Add Task'), findsWidgets); + expect(find.text('Name'), findsOneWidget); + expect(find.text('Done'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); + }); + + testWidgets('shows Edit Task title when editing existing task', + (WidgetTester tester) async { + const existing = Task( + id: '1', + title: 'Existing task', + done: true, + deleted: false, + ); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () => showAddTaskDialog(context, existing), + child: const Text('Open'), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(find.text('Edit Task'), findsWidgets); + expect(find.text('Existing task'), findsOneWidget); + }); + + testWidgets('cancel returns null', (WidgetTester tester) async { + Task? result; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + result = await showAddTaskDialog(context); + }, + child: const Text('Open'), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(result, isNull); + }); + + testWidgets('submitting returns a Task', (WidgetTester tester) async { + Task? result; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + result = await showAddTaskDialog(context); + }, + child: const Text('Open'), + ), + ), + ), + ); - // // Verify that our counter starts at 0. - // expect(find.text('0'), findsOneWidget); - // expect(find.text('1'), findsNothing); + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); - // // Tap the '+' icon and trigger a frame. - // await tester.tap(find.byIcon(Icons.add)); - // await tester.pump(); + await tester.enterText(find.byType(TextField), 'My new task'); + await tester.tap(find.text('Add Task').last); + await tester.pumpAndSettle(); - // // Verify that our counter has incremented. - // expect(find.text('0'), findsNothing); - // expect(find.text('1'), findsOneWidget); + expect(result, isNotNull); + expect(result!.title, 'My new task'); + expect(result!.done, false); + expect(result!.deleted, false); + }); }); } diff --git a/flutter_app/windows/CMakeLists.txt b/flutter_app/windows/CMakeLists.txt index 6c87c1c44..8943541fe 100644 --- a/flutter_app/windows/CMakeLists.txt +++ b/flutter_app/windows/CMakeLists.txt @@ -39,9 +39,9 @@ add_definitions(-DUNICODE -D_UNICODE) # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /W5 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=1") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() @@ -64,7 +64,7 @@ include(flutter/generated_plugins.cmake) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 2) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif()