diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 000000000..b987073ac --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.29.3" +} \ No newline at end of file diff --git a/.github/browserstack-devices.yml b/.github/browserstack-devices.yml index cf5a53643..9b13f4584 100644 --- a/.github/browserstack-devices.yml +++ b/.github/browserstack-devices.yml @@ -43,7 +43,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 b67691eca..3322c9948 100644 --- a/flutter_app/README.md +++ b/flutter_app/README.md @@ -7,7 +7,7 @@ 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) +- Flutter SDK installed (tested on 3.29) - Java Virtual Machine (JVM) 11 installed - Git command line installed (Windows requirement) - XCode installed (for iOS development) @@ -15,6 +15,14 @@ This is a basic task application that demonstrates how to use Ditto's peer-to-pe - Android SDK installed - IDE of choice (Android Studio, VS Code, etc) +### MacOS Development +This has been tested with XCode 26.2 on MacOS 26.6 with Flutter 3.29.3. + +### 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 +86,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 5c8331588..1ef3bfd94 100644 --- a/flutter_app/ios/Podfile.lock +++ b/flutter_app/ios/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (4.13.1): - - DittoFlutter (= 4.13.1) + - ditto_live (5.0.0-rc.3): + - DittoFlutter (= 5.0.0-rc.3) - Flutter - - DittoFlutter (4.13.1) + - DittoFlutter (5.0.0-rc.3) - Flutter (1.0.0) - integration_test (0.0.1): - Flutter @@ -36,12 +36,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: - ditto_live: 93459c7d7c067ba16d4104925ea80c54dd13bf67 - DittoFlutter: 26e21d5665e9bcc11660c4eceb1ec66b8ba64667 + ditto_live: 5b1a156cc731d551555f013001c220b18b3b4c8f + DittoFlutter: 12548a985a137077dd085e91d1c97c4705dfaeb2 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PODFILE CHECKSUM: 1959d098c91d8a792531a723c4a9d7e9f6a01e38 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..15cada483 100644 --- a/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -51,7 +51,7 @@ 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..8182dc317 100644 --- a/flutter_app/lib/main.dart +++ b/flutter_app/lib/main.dart @@ -29,7 +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 authUrl = + dotenv.env['DITTO_AUTH_URL'] ?? (throw Exception("env not found")); final websocketUrl = dotenv.env['DITTO_WEBSOCKET_URL'] ?? (throw Exception("env not found")); @@ -70,26 +71,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 +173,20 @@ 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()); } }, ); + //TODO review to see if we want to add in the order by title asc back in by making the dql builder use two queries. 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 ffbf53676..8c66002a1 100644 --- a/flutter_app/macos/Podfile.lock +++ b/flutter_app/macos/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (4.13.1): - - DittoFlutter (= 4.13.1) + - ditto_live (5.0.0-rc.3): + - DittoFlutter (= 5.0.0-rc.3) - FlutterMacOS - - DittoFlutter (4.13.1) + - DittoFlutter (5.0.0-rc.3) - 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: a46b3eba63227c95adffe6094d3153a74c060d0c - DittoFlutter: 26e21d5665e9bcc11660c4eceb1ec66b8ba64667 + ditto_live: 670ed0ec2cac84febbe3b5fa49a3db6dae38779c + DittoFlutter: 12548a985a137077dd085e91d1c97c4705dfaeb2 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + 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 563838639..fa1eb6d7e 100644 --- a/flutter_app/pubspec.lock +++ b/flutter_app/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" boolean_selector: dependency: transitive description: @@ -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,34 +69,34 @@ packages: dependency: "direct main" description: name: ditto_live - sha256: f095d52ec464e0c50323a4c37717a4e4727c00c035be8e11c0120b5bac927103 + sha256: "5b8571f77a2681529613192acb897a9807033349a967e892cf79327e8750111f" url: "https://pub.dev" source: hosted - version: "4.13.1" + version: "5.0.0-rc.3" 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: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: 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: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 url: "https://pub.dev" source: hosted - version: "5.2.1" + version: "6.0.0" flutter_driver: dependency: transitive description: flutter @@ -127,10 +127,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.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 @@ -179,10 +171,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -203,18 +195,18 @@ packages: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.1.1" 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: @@ -448,10 +440,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: @@ -464,10 +456,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.0.4" xdg_directories: dependency: transitive description: @@ -477,5 +469,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 0f2d5011e..06e8e7b0e 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -32,15 +32,15 @@ dependencies: flutter: sdk: flutter - ditto_live: 4.13.1 + ditto_live: 5.0.0-rc.3 # 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: ^5.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()