From 1ef11e3d2ffa04ed79bf1f23e81ce884faad0745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 26 Sep 2023 17:00:49 +0100 Subject: [PATCH 01/43] chore: Update ios and android files. #275 --- android/build.gradle | 2 +- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.lock | 38 +++++++++++-------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 58a8c74b..713d7f6e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f8f59af7..b8c27560 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a3..a6b826db 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.7.0" From 2c05973e839dd47b4cfa4044d430ab4194b7db7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 27 Sep 2023 15:48:15 +0100 Subject: [PATCH 02/43] chore: Installing dependencies --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 ++ linux/flutter/generated_plugin_registrant.cc | 16 + linux/flutter/generated_plugins.cmake | 4 + macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 14 + macos/Podfile | 43 ++ pubspec.lock | 728 +++++++++++++++++- pubspec.yaml | 20 +- .../flutter/generated_plugin_registrant.cc | 12 + windows/flutter/generated_plugins.cmake | 4 + 13 files changed, 881 insertions(+), 8 deletions(-) create mode 100644 ios/Podfile create mode 100644 macos/Podfile diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..fdcc671e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d2..d560677d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,22 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin"); + emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) pasteboard_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); + pasteboard_plugin_register_with_registrar(pasteboard_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87a..620300de 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter + file_selector_linux + pasteboard + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b6..4b81f9b2 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b6..5caa9d15 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817a..27b7c556 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,20 @@ import FlutterMacOS import Foundation +import device_info_plus +import emoji_picker_flutter +import file_selector_macos +import pasteboard +import path_provider_foundation +import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 00000000..c795730d --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock index 3d683dfa..755fad93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" clock: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb + url: "https://pub.dev" + source: hosted + version: "0.3.3+5" crypto: dependency: transitive description: @@ -121,6 +137,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" + url: "https://pub.dev" + source: hosted + version: "9.0.3" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" diff_match_patch: dependency: transitive description: @@ -129,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + emoji_picker_flutter: + dependency: "direct main" + description: + name: emoji_picker_flutter + sha256: "1ca31245cc1f7ab5304c68ccda8039f52b9f2372aa4d10803117160fad3faf12" + url: "https://pub.dev" + source: hosted + version: "1.6.1" equatable: dependency: "direct main" description: @@ -145,14 +193,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + url: "https://pub.dev" + source: hosted + version: "5.5.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" flutter: dependency: "direct main" description: flutter @@ -166,6 +262,70 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + flutter_colorpicker: + dependency: transitive + description: + name: flutter_colorpicker + sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: f73505c792cf083d5566e1a94002311be497d984b5607f25be36d685cf6361cf + url: "https://pub.dev" + source: hosted + version: "5.7.2+3" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" + source: hosted + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: @@ -174,11 +334,61 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_math_fork: + dependency: transitive + description: + name: flutter_math_fork + sha256: a143a3a89131b578043ecbdb5e759c1033a1b3e9174f5cd1b979d93f4a7fb41c + url: "https://pub.dev" + source: hosted + version: "0.7.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + url: "https://pub.dev" + source: hosted + version: "2.0.16" + flutter_quill: + dependency: "direct main" + description: + name: flutter_quill + sha256: "6350f7b93bd5dc30d12f8235c32f66727c4844f328a0e91869e36ecda4fdb2fa" + url: "https://pub.dev" + source: hosted + version: "7.4.7" + flutter_quill_extensions: + dependency: "direct main" + description: + name: flutter_quill_extensions + sha256: "2881381283c2f31697ba0a5ce4f1b4eb1639d33b10c4252c0de601e4ae640347" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -187,6 +397,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + gallery_saver: + dependency: transitive + description: + name: gallery_saver + sha256: df8b7e207ca12d64c71e0710a7ee3bc48aa7206d51cc720716fedb1543a66712 + url: "https://pub.dev" + source: hosted + version: "2.3.2" glob: dependency: transitive description: @@ -195,6 +413,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + holding_gesture: + dependency: transitive + description: + name: holding_gesture + sha256: beb26bb731d7d67595c4895b42fa7962c209cecee8def42b665c495648d4620f + url: "https://pub.dev" + source: hosted + version: "1.2.0" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" http_multi_server: dependency: transitive description: @@ -204,13 +446,93 @@ packages: source: hosted version: "3.2.1" http_parser: - dependency: transitive + dependency: "direct main" description: name: http_parser sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted version: "4.0.2" + i18n_extension: + dependency: transitive + description: + name: i18n_extension + sha256: db45cd88cf3114f5b9368d975aebebe4ac37fa634fbc5643634289cdfd4d3631 + url: "https://pub.dev" + source: hosted + version: "9.0.2" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "0c7b83bbe2980c8a8e36e974f055e11e51675784e13a4762889feed0f3937ff2" + url: "https://pub.dev" + source: hosted + version: "0.8.8+1" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" io: dependency: transitive description: @@ -267,6 +589,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + math_expressions: + dependency: transitive + description: + name: math_expressions + sha256: "3576593617c3870d75728a751f6ec6e606706d44e363f088ac394b5a28a98064" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + math_keyboard: + dependency: transitive + description: + name: math_keyboard + sha256: "4f5d4eb4b7f003715e2ae7d1f58f5f1fddd9dd746d1f1bbc27f1c967e10124d4" + url: "https://pub.dev" + source: hosted + version: "0.2.1" meta: dependency: transitive description: @@ -276,7 +614,7 @@ packages: source: hosted version: "1.9.1" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e @@ -315,14 +653,118 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - path: + pasteboard: dependency: transitive + description: + name: pasteboard + sha256: "1c8b6a8b3f1d12e55d4e9404433cda1b4abe66db6b17bc2d2fb5965772c04674" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + path: + dependency: "direct main" description: name: path sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + photo_view: + dependency: transitive + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + platform: + dependency: transitive + description: + name: platform + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" pool: dependency: transitive description: @@ -347,6 +789,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" responsive_framework: dependency: "direct main" description: @@ -355,6 +805,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + url: "https://pub.dev" + source: hosted + version: "2.3.1" shelf: dependency: transitive description: @@ -416,6 +922,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -440,6 +954,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + string_validator: + dependency: transitive + description: + name: string_validator + sha256: b419cf5d21d608522e6e7cafed4deb34b6f268c43df866e63c320bab98a08cf6 + url: "https://pub.dev" + source: hosted + version: "1.0.0" term_glyph: dependency: transitive description: @@ -472,6 +994,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.3" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -480,6 +1010,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_html: + dependency: "direct main" + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: "direct main" + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + url: "https://pub.dev" + source: hosted + version: "6.1.14" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + url: "https://pub.dev" + source: hosted + version: "3.0.6" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + url: "https://pub.dev" + source: hosted + version: "3.0.7" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + url: "https://pub.dev" + source: hosted + version: "2.0.20" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + url: "https://pub.dev" + source: hosted + version: "3.0.8" uuid: dependency: "direct main" description: @@ -488,6 +1098,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + url: "https://pub.dev" + source: hosted + version: "1.1.7" vector_math: dependency: transitive description: @@ -496,6 +1130,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: transitive + description: + name: video_player + sha256: "74b86e63529cf5885130c639d74cd2f9232e7c8a66cbecbddd1dcb9dbd060d1e" + url: "https://pub.dev" + source: hosted + version: "2.7.2" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "3fe89ab07fdbce786e7eb25b58532d6eaf189ceddc091cb66cba712f8d9e8e55" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: bf1a1322bf68bccd349982ba1f5a41314a3880861fb9a93d25d6d0a2345845f0 + url: "https://pub.dev" + source: hosted + version: "2.4.11" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a + url: "https://pub.dev" + source: hosted + version: "6.2.1" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "2dd24f7ba46bfb5d070e9c795001db95e0ca5f2a3d025e98f287c10c9f0fd62f" + url: "https://pub.dev" + source: hosted + version: "2.1.1" vm_service: dependency: transitive description: @@ -536,6 +1210,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" yaml: dependency: transitive description: @@ -544,6 +1250,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + youtube_player_flutter: + dependency: transitive + description: + name: youtube_player_flutter + sha256: "72d487e1a1b9155a2dc9d448c137380791101a0ff623723195275ac275ac6942" + url: "https://pub.dev" + source: hosted + version: "8.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2f4c5845..89e14391 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,16 +7,34 @@ repository: https://github.com/dwyl/app issue_tracker: https://github.com/dwyl/app/issues environment: - sdk: '>=2.19.2 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: flutter: sdk: flutter + + # Bloc dependencies flutter_bloc: ^8.1.3 bloc_test: ^9.1.3 equatable: ^2.0.5 + + # Flutter Quill + flutter_quill: ^7.4.7 + flutter_quill_extensions: ^0.4.1 + file_picker: ^5.3.3 + universal_io: ^2.2.2 + universal_html: ^2.2.3 + path: ^1.8.3 + path_provider: ^2.1.0 + http: ^0.13.6 + mime: ^1.0.4 + http_parser: ^4.0.2 + + emoji_picker_flutter: ^1.6.1 uuid: ^3.0.7 responsive_framework: ^1.1.0 + + # Logging lumberdash: ^3.0.0 colorize_lumberdash: ^3.0.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d4680..e94be21a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,18 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + EmojiPickerFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + PasteboardPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PasteboardPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c30..0231913d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter + file_selector_windows + pasteboard + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 43f87b37412e2a3c8c650e20b181f666ce3ce65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 27 Sep 2023 15:48:30 +0100 Subject: [PATCH 03/43] chore: Sealing classes according to Dart 3.0. #275 --- lib/blocs/todo/todo_event.dart | 2 +- lib/blocs/todo/todo_state.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/blocs/todo/todo_event.dart b/lib/blocs/todo/todo_event.dart index 60e6fcf1..6bf2cd8e 100644 --- a/lib/blocs/todo/todo_event.dart +++ b/lib/blocs/todo/todo_event.dart @@ -1,6 +1,6 @@ part of 'todo_bloc.dart'; -abstract class TodoEvent extends Equatable { +sealed class TodoEvent extends Equatable { const TodoEvent(); } diff --git a/lib/blocs/todo/todo_state.dart b/lib/blocs/todo/todo_state.dart index 01e3051b..2f61caec 100644 --- a/lib/blocs/todo/todo_state.dart +++ b/lib/blocs/todo/todo_state.dart @@ -1,6 +1,6 @@ part of 'todo_bloc.dart'; -abstract class TodoState extends Equatable { +sealed class TodoState extends Equatable { const TodoState(); } From 06387881ec7546bfe712054ced41e5ab3f24953d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 27 Sep 2023 16:32:06 +0100 Subject: [PATCH 04/43] chore: Adding app cubit to manage `isWeb` platform bool. #275 --- android/app/build.gradle | 4 +++- lib/blocs/cubit/app_cubit.dart | 8 ++++++++ lib/blocs/cubit/app_state.dart | 14 ++++++++++++++ lib/main.dart | 9 +++++++-- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 lib/blocs/cubit/app_cubit.dart create mode 100644 lib/blocs/cubit/app_state.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index f314154f..6cd87e7c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,9 @@ android { applicationId "com.dwyl.app" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + + // Changed because of `gallery_saver` of `flutter_quill`. Though, it will be switched with `gal` in a different PR. Change this back once that occurs + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/blocs/cubit/app_cubit.dart b/lib/blocs/cubit/app_cubit.dart new file mode 100644 index 00000000..5f96b880 --- /dev/null +++ b/lib/blocs/cubit/app_cubit.dart @@ -0,0 +1,8 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'app_state.dart'; + +class AppCubit extends Cubit { + AppCubit({required bool isWeb}) : super(AppInitial(isWeb: isWeb)); +} diff --git a/lib/blocs/cubit/app_state.dart b/lib/blocs/cubit/app_state.dart new file mode 100644 index 00000000..24b7ccc3 --- /dev/null +++ b/lib/blocs/cubit/app_state.dart @@ -0,0 +1,14 @@ +part of 'app_cubit.dart'; + +sealed class AppState extends Equatable { + final bool isWeb; + + const AppState({required this.isWeb}); + + @override + List get props => []; +} + +final class AppInitial extends AppState { + const AppInitial({required super.isWeb}); +} diff --git a/lib/main.dart b/lib/main.dart index 73d0a59d..d75d72f1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ +import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; import 'package:dwyl_app/logging/logging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:responsive_framework/responsive_framework.dart'; @@ -25,8 +27,11 @@ class MainApp extends StatelessWidget { Bloc.observer = GlobalLogBlocObserver(); putLumberdashToWork(withClients: [ColorizeLumberdash()]); - return BlocProvider( - create: (context) => TodoBloc()..add(TodoListStarted()), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: kIsWeb)), + ], child: MaterialApp( home: const HomePage(), builder: (context, child) => ResponsiveBreakpoints.builder( From deaf1911d4478ada5ce9e7231948223acb890ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 27 Sep 2023 16:51:58 +0100 Subject: [PATCH 05/43] feat: Adding TodoEditor with AppCubit. #275 --- .../widgets/editor/emoji_picker_widget.dart | 64 +++ .../widgets/editor/todo_editor.dart | 461 ++++++++++++++++++ .../web_embeds/mobile_platform_registry.dart | 6 + .../widgets/editor/web_embeds/web_embeds.dart | 90 ++++ .../web_embeds/web_platform_registry.dart | 10 + lib/presentation/widgets/widgets.dart | 1 + 6 files changed, 632 insertions(+) create mode 100644 lib/presentation/widgets/editor/emoji_picker_widget.dart create mode 100644 lib/presentation/widgets/editor/todo_editor.dart create mode 100644 lib/presentation/widgets/editor/web_embeds/mobile_platform_registry.dart create mode 100644 lib/presentation/widgets/editor/web_embeds/web_embeds.dart create mode 100644 lib/presentation/widgets/editor/web_embeds/web_platform_registry.dart diff --git a/lib/presentation/widgets/editor/emoji_picker_widget.dart b/lib/presentation/widgets/editor/emoji_picker_widget.dart new file mode 100644 index 00000000..45b71e1e --- /dev/null +++ b/lib/presentation/widgets/editor/emoji_picker_widget.dart @@ -0,0 +1,64 @@ +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + +const emojiPickerWidgetKey = Key('emojiPickerWidgetKey'); + +/// Emoji picker widget that is offstage. +/// Shows an emoji picker when [offstageEmojiPicker] is `false`. +class OffstageEmojiPicker extends StatefulWidget { + /// `QuillController` controller that is passed so the controller document is changed when emojis are inserted. + final QuillController? quillController; + + /// Determines if the emoji picker is offstage or not. + final bool offstageEmojiPicker; + + const OffstageEmojiPicker({required this.offstageEmojiPicker, this.quillController, super.key}); + + @override + State createState() => _OffstageEmojiPickerState(); +} + +class _OffstageEmojiPickerState extends State { + /// Returns the emoji picker configuration according to screen size. + Config _buildEmojiPickerConfig(BuildContext context) { + if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) { + return const Config(emojiSizeMax: 32.0, columns: 7); + } + + if (ResponsiveBreakpoints.of(context).equals(TABLET)) { + return const Config(emojiSizeMax: 24.0, columns: 10); + } + + if (ResponsiveBreakpoints.of(context).equals(DESKTOP)) { + return const Config(emojiSizeMax: 16.0, columns: 15); + } + + return const Config(emojiSizeMax: 16.0, columns: 30); + } + + @override + Widget build(BuildContext context) { + return Offstage( + offstage: widget.offstageEmojiPicker, + child: SizedBox( + height: 250, + child: EmojiPicker( + key: emojiPickerWidgetKey, + onEmojiSelected: (category, emoji) { + if (widget.quillController != null) { + // Get pointer selection and insert emoji there + final selection = widget.quillController?.selection; + widget.quillController?.document.insert(selection!.end, emoji.emoji); + + // Update the pointer after the emoji we've just inserted + widget.quillController?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); + } + }, + config: _buildEmojiPickerConfig(context), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart new file mode 100644 index 00000000..bce8171d --- /dev/null +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -0,0 +1,461 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:dwyl_app/presentation/widgets/editor/emoji_picker_widget.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_quill/extensions.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:http/http.dart' as http; +import 'package:mime/mime.dart'; +import 'package:http_parser/http_parser.dart'; + +import 'web_embeds/web_embeds.dart'; + +const quillEditorKey = Key('quillEditorKey'); +const emojiButtonKey = Key('emojiButtonKey'); + + +/// Types of selection that person can make when triple clicking +enum _SelectionType { + none, + word, +} + +/// Home page with the `flutter-quill` editor +class DeltaTodoEditor extends StatefulWidget { + final bool isWeb; + + const DeltaTodoEditor({ + required this.isWeb, + super.key, + }); + + @override + DeltaTodoEditorState createState() => DeltaTodoEditorState(); +} + +class DeltaTodoEditorState extends State { + /// `flutter-quill` editor controller + QuillController? _controller; + + /// Focus node used to obtain keyboard focus and events + final FocusNode _focusNode = FocusNode(); + + /// Selection types for triple clicking + _SelectionType _selectionType = _SelectionType.none; + + /// Show emoji picker + bool _offstageEmojiPickerOffstage = true; + + @override + void initState() { + super.initState(); + _initializeText(); + } + + /// Initializing the [Delta](https://quilljs.com/docs/delta/) document with sample text. + Future _initializeText() async { + // final doc = Document()..insert(0, 'Just a friendly empty text :)'); + final doc = Document(); + setState(() { + _controller = QuillController( + document: doc, + selection: const TextSelection.collapsed(offset: 0), + ); + }); + } + + @override + Widget build(BuildContext context) { + /// Loading widget if controller's not loaded + if (_controller == null) { + return const Scaffold(body: Center(child: Text('Loading...'))); + } + + /// Returning scaffold with editor as body + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: false, + title: const Text( + 'Flutter Quill', + ), + ), + body: _buildEditor(context), + ); + } + + /// Callback called whenever the person taps on the text. + /// It will select nothing, then the word if another tap is detected + /// and then the whole text if another tap is detected (triple). + bool _onTripleClickSelection() { + final controller = _controller!; + + // If nothing is selected, selection type is `none` + if (controller.selection.isCollapsed) { + _selectionType = _SelectionType.none; + } + + // If nothing is selected, selection type becomes `word + if (_selectionType == _SelectionType.none) { + _selectionType = _SelectionType.word; + return false; + } + + // If the word is selected, select all text + if (_selectionType == _SelectionType.word) { + final child = controller.document.queryChild( + controller.selection.baseOffset, + ); + final offset = child.node?.documentOffset ?? 0; + final length = child.node?.length ?? 0; + + final selection = TextSelection( + baseOffset: offset, + extentOffset: offset + length, + ); + + // Select all text and make next selection to `none` + controller.updateSelection(selection, ChangeSource.REMOTE); + + _selectionType = _SelectionType.none; + + return true; + } + + return false; + } + + /// Callback called whenever the person taps on the emoji button in the toolbar. + /// It shows/hides the emoji picker and focus/unfocusses the keyboard accordingly. + void _onEmojiButtonPressed(BuildContext context) { + final isEmojiPickerShown = !_offstageEmojiPickerOffstage; + + // If emoji picker is being shown, we show the keyboard and hide the emoji picker. + if (isEmojiPickerShown) { + _focusNode.requestFocus(); + setState(() { + _offstageEmojiPickerOffstage = true; + }); + } + + // Otherwise, we do the inverse. + else { + // Unfocusing when the person clicks away. This is to hide the keyboard. + // See https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ + // and https://www.youtube.com/watch?v=MKrEJtheGPk&t=40s&ab_channel=HeyFlutter%E2%80%A4com. + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + //currentFocus.unfocus(); + } + + setState(() { + _offstageEmojiPickerOffstage = false; + }); + } + } + + /// Build the `flutter-quill` editor to be shown on screen. + Widget _buildEditor(BuildContext context) { + // Default editor (for mobile devices) + Widget quillEditor = QuillEditor( + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Write what\'s on your mind.', + enableSelectionToolbar: isMobile(), + expands: false, + padding: EdgeInsets.zero, + onTapDown: (details, p1) { + // When the person taps on the text, we want to hide the emoji picker + // so only the keyboard is shown + setState(() { + _offstageEmojiPickerOffstage = true; + }); + return false; + }, + onTapUp: (details, p1) { + return _onTripleClickSelection(); + }, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), + null, + ), + h2: DefaultTextBlockStyle( + const TextStyle( + fontSize: 24, + color: Colors.black87, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + h3: DefaultTextBlockStyle( + const TextStyle( + fontSize: 20, + color: Colors.black87, + height: 1.25, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + sizeSmall: const TextStyle(fontSize: 9), + subscript: const TextStyle( + fontFamily: 'SF-UI-Display', + fontFeatures: [FontFeature.subscripts()], + ), + superscript: const TextStyle( + fontFamily: 'SF-UI-Display', + fontFeatures: [FontFeature.superscripts()], + ), + ), + embedBuilders: [...FlutterQuillEmbeds.builders()], + ); + + // Alternatively, the web editor version is shown (with the web embeds) + if (widget.isWeb) { + quillEditor = QuillEditor( + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + expands: false, + padding: EdgeInsets.zero, + onTapUp: (details, p1) { + return _onTripleClickSelection(); + }, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), + null, + ), + h2: DefaultTextBlockStyle( + const TextStyle( + fontSize: 24, + color: Colors.black87, + height: 1.15, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + h3: DefaultTextBlockStyle( + const TextStyle( + fontSize: 20, + color: Colors.black87, + height: 1.25, + fontWeight: FontWeight.w600, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), + sizeSmall: const TextStyle(fontSize: 9), + ), + embedBuilders: [...defaultEmbedBuildersWeb], + ); + } + + // Toolbar definitions + const toolbarIconSize = 18.0; + final embedButtons = FlutterQuillEmbeds.buttons( + // Showing only necessary default buttons + showCameraButton: false, + showFormulaButton: false, + showVideoButton: false, + showImageButton: true, + + // `onImagePickCallback` is called after image is picked on mobile platforms + onImagePickCallback: _onImagePickCallback, + + // `webImagePickImpl` is called after image is picked on the web + webImagePickImpl: _webImagePickImpl, + + // defining the selector (we only want to open the gallery whenever the person wants to upload an image) + mediaPickSettingSelector: (context) { + return Future.value(MediaPickSetting.Gallery); + }, + ); + + // Instantiating the toolbar + final toolbar = QuillToolbar( + afterButtonPressed: _focusNode.requestFocus, + children: [ + CustomButton( + key: emojiButtonKey, + onPressed: () => _onEmojiButtonPressed(context), + icon: Icons.emoji_emotions, + iconSize: toolbarIconSize, + ), + HistoryButton( + icon: Icons.undo_outlined, + iconSize: toolbarIconSize, + controller: _controller!, + undo: true, + ), + HistoryButton( + icon: Icons.redo_outlined, + iconSize: toolbarIconSize, + controller: _controller!, + undo: false, + ), + SelectHeaderStyleButton( + controller: _controller!, + axis: Axis.horizontal, + iconSize: toolbarIconSize, + attributes: const [Attribute.h1, Attribute.h2, Attribute.h3], + ), + ToggleStyleButton( + attribute: Attribute.bold, + icon: Icons.format_bold, + iconSize: toolbarIconSize, + controller: _controller!, + ), + ToggleStyleButton( + attribute: Attribute.italic, + icon: Icons.format_italic, + iconSize: toolbarIconSize, + controller: _controller!, + ), + ToggleStyleButton( + attribute: Attribute.underline, + icon: Icons.format_underline, + iconSize: toolbarIconSize, + controller: _controller!, + ), + ToggleStyleButton( + attribute: Attribute.strikeThrough, + icon: Icons.format_strikethrough, + iconSize: toolbarIconSize, + controller: _controller!, + ), + LinkStyleButton( + controller: _controller!, + iconSize: toolbarIconSize, + linkRegExp: RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'), + ), + for (final builder in embedButtons) builder(_controller!, toolbarIconSize, null, null), + ], + ); + + // Rendering the final editor + toolbar + return SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 15, + child: Container( + key: quillEditorKey, + color: Colors.white, + padding: const EdgeInsets.only(left: 16, right: 16), + child: quillEditor, + ), + ), + Container(child: toolbar), + OffstageEmojiPicker( + offstageEmojiPicker: _offstageEmojiPickerOffstage, + quillController: _controller, + ), + ], + ), + ); + } + + /// Renders the image picked by imagePicker from local file storage + /// You can also upload the picked image to any server (eg : AWS s3 + /// or Firebase) and then return the uploaded image URL. + /// + /// It's only called on mobile platforms. + Future _onImagePickCallback(File file) async { + final appDocDir = await getApplicationDocumentsDirectory(); + final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}'); + return copiedFile.path.toString(); + } + + /// Callback that is called after an image is picked whilst on the web platform. + /// Returns the URL of the image. + /// Returns null if an error occurred uploading the file or the image was not picked. + Future _webImagePickImpl(OnImagePickCallback onImagePickCallback) async { + // Lets the user pick one file; files with any file extension can be selected + final result = await ImageFilePicker().pickImage(); + + // The result will be null, if the user aborted the dialog + if (result == null || result.files.isEmpty) { + return null; + } + + // Read file as bytes (https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ#q-how-do-i-access-the-path-on-web) + final platformFile = result.files.first; + final bytes = platformFile.bytes; + + if (bytes == null) { + return null; + } + + // Make HTTP request to upload the image to the file + const apiURL = 'https://imgup.fly.dev/api/images'; + final request = http.MultipartRequest('POST', Uri.parse(apiURL)); + + final httpImage = http.MultipartFile.fromBytes( + 'image', + bytes, + contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), + filename: platformFile.name, + ); + request.files.add(httpImage); + + // Check the response and handle accordingly + return http.Client().send(request).then((response) async { + if (response.statusCode != 200) { + return null; + } + + final responseStream = await http.Response.fromStream(response); + final responseData = json.decode(responseStream.body); + return responseData['url']; + }); + } +} + +// coverage:ignore-start +/// Image file picker wrapper class +class ImageFilePicker { + Future pickImage() => FilePicker.platform.pickFiles(type: FileType.image); +} +// coverage:ignore-end diff --git a/lib/presentation/widgets/editor/web_embeds/mobile_platform_registry.dart b/lib/presentation/widgets/editor/web_embeds/mobile_platform_registry.dart new file mode 100644 index 00000000..6a94e7e4 --- /dev/null +++ b/lib/presentation/widgets/editor/web_embeds/mobile_platform_registry.dart @@ -0,0 +1,6 @@ +/// Class used to `registerViewFactory` for mobile platforms. +/// +/// Please check https://github.com/flutter/flutter/issues/41563#issuecomment-547923478 for more information. +class PlatformViewRegistry { + static void registerViewFactory(String viewId, dynamic cb) {} +} diff --git a/lib/presentation/widgets/editor/web_embeds/web_embeds.dart b/lib/presentation/widgets/editor/web_embeds/web_embeds.dart new file mode 100644 index 00000000..d88c56c7 --- /dev/null +++ b/lib/presentation/widgets/editor/web_embeds/web_embeds.dart @@ -0,0 +1,90 @@ +import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:responsive_framework/responsive_framework.dart'; +import 'package:universal_html/html.dart' as html; + +// Conditionally importing the PlatformViewRegistry class according to the platform +import 'mobile_platform_registry.dart' if (dart.library.html) 'web_platform_registry.dart' as ui_instance; + +/// Class used to conditionally register the view factory. +/// For more information, check https://github.com/flutter/flutter/issues/41563#issuecomment-547923478. +class PlatformViewRegistryFix { + + /// [imageURL] imageURl to be shown. + /// [cbFnc] callback function to be called when the view is created. + /// [isWebPlatform] if the platform is web-based or not. + void registerViewFactory({required bool isWebPlatform, required imageURL, required dynamic cbFnc}) { + if (isWebPlatform) { + ui_instance.PlatformViewRegistry.registerViewFactory( + imageURL, + cbFnc, + ); + } + } +} + +/// Class that conditionally registers the `platformViewRegistry`. +class ImageUniversalUI { + PlatformViewRegistryFix platformViewRegistry = PlatformViewRegistryFix(); +} + +/// Custom embed for images to work on the web. +class ImageEmbedBuilderWeb extends EmbedBuilder { + @override + String get key => BlockEmbed.imageType; + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + final isWeb = BlocProvider.of(context).state.isWeb; + final imageUrl = node.value.data; + + if (isImageBase64(imageUrl)) { + // TODO: handle imageUrl of base64 + return const SizedBox(); + } + final size = MediaQuery.of(context).size; + + // This is needed for images to be correctly embedded on the web. + ImageUniversalUI().platformViewRegistry.registerViewFactory( + isWebPlatform: isWeb, + imageURL: imageUrl, + cbFnc: (viewId) { + return html.ImageElement() + ..src = imageUrl + ..style.height = 'auto' + ..style.width = 'auto'; + }, ); + + // Rendering responsive image + return Padding( + padding: EdgeInsets.only( + right: ResponsiveBreakpoints.of(context).smallerThan(TABLET) + ? size.width * 0.5 + : (ResponsiveBreakpoints.of(context).equals('4K')) + ? size.width * 0.75 + : size.width * 0.2, + ), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.45, + child: HtmlElementView( + viewType: imageUrl, + ), + ), + ); + } +} + +/// List of default web embed builders. +List get defaultEmbedBuildersWeb => [ + ImageEmbedBuilderWeb(), + ]; diff --git a/lib/presentation/widgets/editor/web_embeds/web_platform_registry.dart b/lib/presentation/widgets/editor/web_embeds/web_platform_registry.dart new file mode 100644 index 00000000..af67a223 --- /dev/null +++ b/lib/presentation/widgets/editor/web_embeds/web_platform_registry.dart @@ -0,0 +1,10 @@ +import 'dart:ui_web' as web_ui; + +/// Class used to `registerViewFactory` for web platforms. +/// +/// Please check https://github.com/flutter/flutter/issues/41563#issuecomment-547923478 for more information. +class PlatformViewRegistry { + static void registerViewFactory(String viewId, dynamic cb) { + web_ui.platformViewRegistry.registerViewFactory(viewId, cb); + } +} diff --git a/lib/presentation/widgets/widgets.dart b/lib/presentation/widgets/widgets.dart index 9b9dec16..ddca2fb3 100644 --- a/lib/presentation/widgets/widgets.dart +++ b/lib/presentation/widgets/widgets.dart @@ -1,2 +1,3 @@ export 'items.dart'; export 'navbar.dart'; +export 'editor/todo_editor.dart'; From 091e12e852d52c685f0de2a6cd895a34a541925a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 09:56:14 +0100 Subject: [PATCH 06/43] chore: Fix iOS running issues. #275 --- ios/Podfile | 6 +- ios/Podfile.lock | 145 ++++++++++++++++++ ios/Runner.xcodeproj/project.pbxproj | 68 ++++++++ .../contents.xcworkspacedata | 3 + 4 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 ios/Podfile.lock diff --git a/ios/Podfile b/ios/Podfile index fdcc671e..7cbfd60a 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,9 +32,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end + # target 'RunnerTests' do + # inherit! :search_paths + # end end post_install do |installer| diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..bacf168a --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,145 @@ +PODS: + - device_info_plus (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.4) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - emoji_picker_flutter (0.0.1): + - Flutter + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - flutter_inappwebview (0.0.1): + - Flutter + - flutter_inappwebview/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) + - flutter_keyboard_visibility (0.0.1): + - Flutter + - gallery_saver (0.0.1): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - OrderedSet (5.0.0) + - pasteboard (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SDWebImage (5.18.0): + - SDWebImage/Core (= 5.18.0) + - SDWebImage/Core (5.18.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SwiftyGif (5.4.4) + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + +DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - gallery_saver (from `.symlinks/plugins/gallery_saver/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - pasteboard (from `.symlinks/plugins/pasteboard/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - OrderedSet + - SDWebImage + - SwiftyGif + +EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + flutter_inappwebview: + :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + gallery_saver: + :path: ".symlinks/plugins/gallery_saver/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + pasteboard: + :path: ".symlinks/plugins/pasteboard/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" + +SPEC CHECKSUMS: + device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + emoji_picker_flutter: df19dac03a2b39ac667dc8d1da939ef3a9e21347 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + gallery_saver: 9fc173c9f4fcc48af53b2a9ebea1b643255be542 + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + SDWebImage: 182830bcddc30cde95fbc60dfe4badc3553d94ba + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 + +PODFILE CHECKSUM: c64f963f8da334e55044cf867c3ac33423cdcd9f + +COCOAPODS: 1.12.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b8c27560..d2ee9446 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B8E7B3746F0CB880E4498CDF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE4D87899815EB7E57837BB8 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,10 +32,12 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 162D8AE69F9348E31D007593 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 90AF7D4A3569F8E1D5621536 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +45,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AE4D87899815EB7E57837BB8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F307A77381B535C30F804E38 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B8E7B3746F0CB880E4498CDF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0C8AAB30FA0CE83850F3092F /* Frameworks */ = { + isa = PBXGroup; + children = ( + AE4D87899815EB7E57837BB8 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +86,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + D8BD25645EEAD585F12259BD /* Pods */, + 0C8AAB30FA0CE83850F3092F /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +114,17 @@ path = Runner; sourceTree = ""; }; + D8BD25645EEAD585F12259BD /* Pods */ = { + isa = PBXGroup; + children = ( + 90AF7D4A3569F8E1D5621536 /* Pods-Runner.debug.xcconfig */, + 162D8AE69F9348E31D007593 /* Pods-Runner.release.xcconfig */, + F307A77381B535C30F804E38 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 2A4C4359A22ED34A06B57A4D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ADBA25C8FAC669C056D8B307 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +198,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2A4C4359A22ED34A06B57A4D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -200,6 +251,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + ADBA25C8FAC669C056D8B307 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + From 6de1dc09f3b75b8c357b87f6c0c5ab29e464db33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 11:44:08 +0100 Subject: [PATCH 07/43] feat: Adding the todo editor on page navigation. #275 --- lib/presentation/views/new_todo.dart | 58 ++++----------- .../widgets/editor/todo_editor.dart | 20 ++---- lib/presentation/widgets/navbar.dart | 70 ++++++++++--------- 3 files changed, 58 insertions(+), 90 deletions(-) diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 7022c8b9..4caeeaa8 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -1,3 +1,5 @@ +import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; +import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:responsive_framework/responsive_framework.dart'; @@ -11,8 +13,7 @@ const saveButtonKey = Key('saveButtonKey'); /// Transition handler that navigates the route to the `NewTodo` item page. Route navigateToNewTodoItemPage() { return PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - const NewTodoPage(), + pageBuilder: (context, animation, secondaryAnimation) => const NewTodoPage(), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ); @@ -38,56 +39,28 @@ class _NewTodoPageState extends State { @override Widget build(BuildContext context) { + final isWeb = BlocProvider.of(context).state.isWeb; + return MaterialApp( home: Scaffold( appBar: NavBar( givenContext: context, showGoBackButton: true, + onTap: () { + // Dismiss the keyboard + final currentFocus = FocusScope.of(context); + + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + }, ), body: SafeArea( child: Column( children: [ // Textfield that is expanded and borderless Expanded( - child: (() { - // On mobile - if (ResponsiveBreakpoints.of(context).isMobile) { - return TextField( - key: textfieldOnNewPageKey, - controller: txtFieldController, - expands: true, - maxLines: null, - autofocus: true, - style: const TextStyle(fontSize: 20), - decoration: const InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.zero, - ), - hintText: 'start typing', - ), - textAlignVertical: TextAlignVertical.top, - ); - } - - // On tablet and up - else { - return TextField( - key: textfieldOnNewPageKey, - controller: txtFieldController, - expands: true, - maxLines: null, - autofocus: true, - style: const TextStyle(fontSize: 30), - decoration: const InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.zero, - ), - hintText: 'start typing', - ), - textAlignVertical: TextAlignVertical.top, - ); - } - }()), + child: DeltaTodoEditor(isWeb: isWeb) ), // Save button. @@ -107,8 +80,7 @@ class _NewTodoPageState extends State { if (value.isNotEmpty) { // Create new item and create AddTodo event final newTodoItem = Item(description: value); - BlocProvider.of(context) - .add(AddTodoEvent(newTodoItem)); + BlocProvider.of(context).add(AddTodoEvent(newTodoItem)); // Clear textfield txtFieldController.clear(); diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index bce8171d..886f9cec 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -21,7 +21,6 @@ import 'web_embeds/web_embeds.dart'; const quillEditorKey = Key('quillEditorKey'); const emojiButtonKey = Key('emojiButtonKey'); - /// Types of selection that person can make when triple clicking enum _SelectionType { none, @@ -76,21 +75,11 @@ class DeltaTodoEditorState extends State { Widget build(BuildContext context) { /// Loading widget if controller's not loaded if (_controller == null) { - return const Scaffold(body: Center(child: Text('Loading...'))); + return const Center(child: Text('Loading editor..')); } /// Returning scaffold with editor as body - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - centerTitle: false, - title: const Text( - 'Flutter Quill', - ), - ), - body: _buildEditor(context), - ); + return _buildEditor(context); } /// Callback called whenever the person taps on the text. @@ -177,7 +166,7 @@ class DeltaTodoEditorState extends State { placeholder: 'Write what\'s on your mind.', enableSelectionToolbar: isMobile(), expands: false, - padding: EdgeInsets.zero, + padding: const EdgeInsets.only(top: 16.0), onTapDown: (details, p1) { // When the person taps on the text, we want to hide the emoji picker // so only the keyboard is shown @@ -247,7 +236,7 @@ class DeltaTodoEditorState extends State { readOnly: false, placeholder: 'Add content', expands: false, - padding: EdgeInsets.zero, + padding: const EdgeInsets.only(top: 16.0), onTapUp: (details, p1) { return _onTripleClickSelection(); }, @@ -315,6 +304,7 @@ class DeltaTodoEditorState extends State { // Instantiating the toolbar final toolbar = QuillToolbar( afterButtonPressed: _focusNode.requestFocus, + multiRowsDisplay: false, children: [ CustomButton( key: emojiButtonKey, diff --git a/lib/presentation/widgets/navbar.dart b/lib/presentation/widgets/navbar.dart index 4b61d96d..f8c60c69 100644 --- a/lib/presentation/widgets/navbar.dart +++ b/lib/presentation/widgets/navbar.dart @@ -6,49 +6,55 @@ const logoKey = Key('logoKey'); /// Navigation bar widget. /// It needs to receive a context to dynamically elements. class NavBar extends StatelessWidget implements PreferredSizeWidget { - // Boolean that tells the bar to have a button to go to the previous page + /// Boolean that tells the bar to have a button to go to the previous page final bool showGoBackButton; - // Build context for the "go back" button works + /// Build context for the "go back" button works final BuildContext givenContext; + /// Callback for when the user taps on the navbar + final VoidCallback? onTap; const NavBar({ required this.givenContext, super.key, - this.showGoBackButton = false, + this.showGoBackButton = false, + this.onTap, }); @override Widget build(BuildContext context) { - return AppBar( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () { - Navigator.pop(givenContext); - }, - child: - // dwyl logo - Image.asset( - 'assets/icon/icon.png', - key: logoKey, - fit: BoxFit.fitHeight, - height: 30, - ), - ), - ], - ), - backgroundColor: const Color.fromARGB(255, 81, 72, 72), - elevation: 0.0, - centerTitle: true, - leading: showGoBackButton - ? BackButton( - key: backButtonKey, - onPressed: () { + return GestureDetector( + onTap: onTap, + child: AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { Navigator.pop(givenContext); }, - ) - : null, + child: + // dwyl logo + Image.asset( + 'assets/icon/icon.png', + key: logoKey, + fit: BoxFit.fitHeight, + height: 30, + ), + ), + ], + ), + backgroundColor: const Color.fromARGB(255, 81, 72, 72), + elevation: 0.0, + centerTitle: true, + leading: showGoBackButton + ? BackButton( + key: backButtonKey, + onPressed: () { + Navigator.pop(givenContext); + }, + ) + : null, + ), ); } From 5f4134a04e0ae3c5fa07fc3eb55d9400d7d57ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 11:50:12 +0100 Subject: [PATCH 08/43] chore: Small refactor on NavBar tap. #275 --- lib/presentation/views/new_todo.dart | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 4caeeaa8..5a86fbbb 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -37,6 +37,17 @@ class _NewTodoPageState extends State { super.dispose(); } + /// Dismisses the keyboard when the user taps on the navbar. + /// + /// See https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ for more implementation details. + void _onTapNavbar(BuildContext context) { + final currentFocus = FocusScope.of(context); + + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + } + @override Widget build(BuildContext context) { final isWeb = BlocProvider.of(context).state.isWeb; @@ -46,22 +57,13 @@ class _NewTodoPageState extends State { appBar: NavBar( givenContext: context, showGoBackButton: true, - onTap: () { - // Dismiss the keyboard - final currentFocus = FocusScope.of(context); - - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } - }, + onTap: () => _onTapNavbar(context), ), body: SafeArea( child: Column( children: [ // Textfield that is expanded and borderless - Expanded( - child: DeltaTodoEditor(isWeb: isWeb) - ), + Expanded(child: DeltaTodoEditor(isWeb: isWeb)), // Save button. // When submitted, it adds a new todo item, clears the controller and navigates back From 69672c16f321ba8db8f13c9a6a994e6fc837f2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 13:08:01 +0100 Subject: [PATCH 09/43] chore: Separating controller from the editor widget. #275 --- lib/presentation/views/new_todo.dart | 19 ++++--- .../widgets/editor/todo_editor.dart | 51 +++++++------------ 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 5a86fbbb..0c3c9d22 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -1,6 +1,7 @@ import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:responsive_framework/responsive_framework.dart'; import '../../blocs/blocs.dart'; @@ -28,12 +29,14 @@ class NewTodoPage extends StatefulWidget { } class _NewTodoPageState extends State { - // https://stackoverflow.com/questions/61425969/is-it-okay-to-use-texteditingcontroller-in-statelesswidget-in-flutter - TextEditingController txtFieldController = TextEditingController(); + final _controller = QuillController( + document: Document(), + selection: const TextSelection.collapsed(offset: 0), + ); @override void dispose() { - txtFieldController.dispose(); + _controller.dispose(); super.dispose(); } @@ -63,7 +66,11 @@ class _NewTodoPageState extends State { child: Column( children: [ // Textfield that is expanded and borderless - Expanded(child: DeltaTodoEditor(isWeb: isWeb)), + Expanded( + child: DeltaTodoEditor( + isWeb: isWeb, + editorController: _controller, + )), // Save button. // When submitted, it adds a new todo item, clears the controller and navigates back @@ -78,14 +85,14 @@ class _NewTodoPageState extends State { ), ), onPressed: () { - final value = txtFieldController.text; + final value = _controller.document.toPlainText(); if (value.isNotEmpty) { // Create new item and create AddTodo event final newTodoItem = Item(description: value); BlocProvider.of(context).add(AddTodoEvent(newTodoItem)); // Clear textfield - txtFieldController.clear(); + _controller.clear(); // Go back to home page Navigator.pop(context); diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index 886f9cec..7f8196b9 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -29,10 +29,15 @@ enum _SelectionType { /// Home page with the `flutter-quill` editor class DeltaTodoEditor extends StatefulWidget { + /// Is the platform web-based? final bool isWeb; + /// Editor controller. Must be a `QuillController` object. + final QuillController editorController; + const DeltaTodoEditor({ required this.isWeb, + required this.editorController, super.key, }); @@ -41,8 +46,6 @@ class DeltaTodoEditor extends StatefulWidget { } class DeltaTodoEditorState extends State { - /// `flutter-quill` editor controller - QuillController? _controller; /// Focus node used to obtain keyboard focus and events final FocusNode _focusNode = FocusNode(); @@ -56,28 +59,10 @@ class DeltaTodoEditorState extends State { @override void initState() { super.initState(); - _initializeText(); - } - - /// Initializing the [Delta](https://quilljs.com/docs/delta/) document with sample text. - Future _initializeText() async { - // final doc = Document()..insert(0, 'Just a friendly empty text :)'); - final doc = Document(); - setState(() { - _controller = QuillController( - document: doc, - selection: const TextSelection.collapsed(offset: 0), - ); - }); } @override Widget build(BuildContext context) { - /// Loading widget if controller's not loaded - if (_controller == null) { - return const Center(child: Text('Loading editor..')); - } - /// Returning scaffold with editor as body return _buildEditor(context); } @@ -86,7 +71,7 @@ class DeltaTodoEditorState extends State { /// It will select nothing, then the word if another tap is detected /// and then the whole text if another tap is detected (triple). bool _onTripleClickSelection() { - final controller = _controller!; + final controller = widget.editorController; // If nothing is selected, selection type is `none` if (controller.selection.isCollapsed) { @@ -157,7 +142,7 @@ class DeltaTodoEditorState extends State { Widget _buildEditor(BuildContext context) { // Default editor (for mobile devices) Widget quillEditor = QuillEditor( - controller: _controller!, + controller: widget.editorController, scrollController: ScrollController(), scrollable: true, focusNode: _focusNode, @@ -228,7 +213,7 @@ class DeltaTodoEditorState extends State { // Alternatively, the web editor version is shown (with the web embeds) if (widget.isWeb) { quillEditor = QuillEditor( - controller: _controller!, + controller: widget.editorController, scrollController: ScrollController(), scrollable: true, focusNode: _focusNode, @@ -315,17 +300,17 @@ class DeltaTodoEditorState extends State { HistoryButton( icon: Icons.undo_outlined, iconSize: toolbarIconSize, - controller: _controller!, + controller: widget.editorController, undo: true, ), HistoryButton( icon: Icons.redo_outlined, iconSize: toolbarIconSize, - controller: _controller!, + controller: widget.editorController, undo: false, ), SelectHeaderStyleButton( - controller: _controller!, + controller: widget.editorController, axis: Axis.horizontal, iconSize: toolbarIconSize, attributes: const [Attribute.h1, Attribute.h2, Attribute.h3], @@ -334,32 +319,32 @@ class DeltaTodoEditorState extends State { attribute: Attribute.bold, icon: Icons.format_bold, iconSize: toolbarIconSize, - controller: _controller!, + controller: widget.editorController, ), ToggleStyleButton( attribute: Attribute.italic, icon: Icons.format_italic, iconSize: toolbarIconSize, - controller: _controller!, + controller: widget.editorController, ), ToggleStyleButton( attribute: Attribute.underline, icon: Icons.format_underline, iconSize: toolbarIconSize, - controller: _controller!, + controller: widget.editorController, ), ToggleStyleButton( attribute: Attribute.strikeThrough, icon: Icons.format_strikethrough, iconSize: toolbarIconSize, - controller: _controller!, + controller: widget.editorController, ), LinkStyleButton( - controller: _controller!, + controller: widget.editorController, iconSize: toolbarIconSize, linkRegExp: RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'), ), - for (final builder in embedButtons) builder(_controller!, toolbarIconSize, null, null), + for (final builder in embedButtons) builder(widget.editorController, toolbarIconSize, null, null), ], ); @@ -380,7 +365,7 @@ class DeltaTodoEditorState extends State { Container(child: toolbar), OffstageEmojiPicker( offstageEmojiPicker: _offstageEmojiPickerOffstage, - quillController: _controller, + quillController: widget.editorController, ), ], ), From 61c73942fee0d7bf4c0fcbb4c9dd688b01c517a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 13:23:15 +0100 Subject: [PATCH 10/43] chore: Small refactoring. #275 --- .../widgets/editor/emoji_picker_widget.dart | 12 +- .../widgets/editor/todo_editor.dart | 151 +++++++++--------- 2 files changed, 78 insertions(+), 85 deletions(-) diff --git a/lib/presentation/widgets/editor/emoji_picker_widget.dart b/lib/presentation/widgets/editor/emoji_picker_widget.dart index 45b71e1e..3717a7cb 100644 --- a/lib/presentation/widgets/editor/emoji_picker_widget.dart +++ b/lib/presentation/widgets/editor/emoji_picker_widget.dart @@ -9,12 +9,12 @@ const emojiPickerWidgetKey = Key('emojiPickerWidgetKey'); /// Shows an emoji picker when [offstageEmojiPicker] is `false`. class OffstageEmojiPicker extends StatefulWidget { /// `QuillController` controller that is passed so the controller document is changed when emojis are inserted. - final QuillController? quillController; + final QuillController? editorController; /// Determines if the emoji picker is offstage or not. final bool offstageEmojiPicker; - const OffstageEmojiPicker({required this.offstageEmojiPicker, this.quillController, super.key}); + const OffstageEmojiPicker({required this.offstageEmojiPicker, this.editorController, super.key}); @override State createState() => _OffstageEmojiPickerState(); @@ -47,13 +47,13 @@ class _OffstageEmojiPickerState extends State { child: EmojiPicker( key: emojiPickerWidgetKey, onEmojiSelected: (category, emoji) { - if (widget.quillController != null) { + if (widget.editorController != null) { // Get pointer selection and insert emoji there - final selection = widget.quillController?.selection; - widget.quillController?.document.insert(selection!.end, emoji.emoji); + final selection = widget.editorController?.selection; + widget.editorController?.document.insert(selection!.end, emoji.emoji); // Update the pointer after the emoji we've just inserted - widget.quillController?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); + widget.editorController?.updateSelection(TextSelection.collapsed(offset: selection!.end + emoji.emoji.length), ChangeSource.REMOTE); } }, config: _buildEmojiPickerConfig(context), diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index 7f8196b9..d95bd564 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -46,7 +46,6 @@ class DeltaTodoEditor extends StatefulWidget { } class DeltaTodoEditorState extends State { - /// Focus node used to obtain keyboard focus and events final FocusNode _focusNode = FocusNode(); @@ -63,83 +62,6 @@ class DeltaTodoEditorState extends State { @override Widget build(BuildContext context) { - /// Returning scaffold with editor as body - return _buildEditor(context); - } - - /// Callback called whenever the person taps on the text. - /// It will select nothing, then the word if another tap is detected - /// and then the whole text if another tap is detected (triple). - bool _onTripleClickSelection() { - final controller = widget.editorController; - - // If nothing is selected, selection type is `none` - if (controller.selection.isCollapsed) { - _selectionType = _SelectionType.none; - } - - // If nothing is selected, selection type becomes `word - if (_selectionType == _SelectionType.none) { - _selectionType = _SelectionType.word; - return false; - } - - // If the word is selected, select all text - if (_selectionType == _SelectionType.word) { - final child = controller.document.queryChild( - controller.selection.baseOffset, - ); - final offset = child.node?.documentOffset ?? 0; - final length = child.node?.length ?? 0; - - final selection = TextSelection( - baseOffset: offset, - extentOffset: offset + length, - ); - - // Select all text and make next selection to `none` - controller.updateSelection(selection, ChangeSource.REMOTE); - - _selectionType = _SelectionType.none; - - return true; - } - - return false; - } - - /// Callback called whenever the person taps on the emoji button in the toolbar. - /// It shows/hides the emoji picker and focus/unfocusses the keyboard accordingly. - void _onEmojiButtonPressed(BuildContext context) { - final isEmojiPickerShown = !_offstageEmojiPickerOffstage; - - // If emoji picker is being shown, we show the keyboard and hide the emoji picker. - if (isEmojiPickerShown) { - _focusNode.requestFocus(); - setState(() { - _offstageEmojiPickerOffstage = true; - }); - } - - // Otherwise, we do the inverse. - else { - // Unfocusing when the person clicks away. This is to hide the keyboard. - // See https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ - // and https://www.youtube.com/watch?v=MKrEJtheGPk&t=40s&ab_channel=HeyFlutter%E2%80%A4com. - final currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - SystemChannels.textInput.invokeMethod('TextInput.hide'); - //currentFocus.unfocus(); - } - - setState(() { - _offstageEmojiPickerOffstage = false; - }); - } - } - - /// Build the `flutter-quill` editor to be shown on screen. - Widget _buildEditor(BuildContext context) { // Default editor (for mobile devices) Widget quillEditor = QuillEditor( controller: widget.editorController, @@ -365,13 +287,84 @@ class DeltaTodoEditorState extends State { Container(child: toolbar), OffstageEmojiPicker( offstageEmojiPicker: _offstageEmojiPickerOffstage, - quillController: widget.editorController, + editorController: widget.editorController, ), ], ), ); } + /// Callback called whenever the person taps on the text. + /// It will select nothing, then the word if another tap is detected + /// and then the whole text if another tap is detected (triple). + bool _onTripleClickSelection() { + final controller = widget.editorController; + + // If nothing is selected, selection type is `none` + if (controller.selection.isCollapsed) { + _selectionType = _SelectionType.none; + } + + // If nothing is selected, selection type becomes `word + if (_selectionType == _SelectionType.none) { + _selectionType = _SelectionType.word; + return false; + } + + // If the word is selected, select all text + if (_selectionType == _SelectionType.word) { + final child = controller.document.queryChild( + controller.selection.baseOffset, + ); + final offset = child.node?.documentOffset ?? 0; + final length = child.node?.length ?? 0; + + final selection = TextSelection( + baseOffset: offset, + extentOffset: offset + length, + ); + + // Select all text and make next selection to `none` + controller.updateSelection(selection, ChangeSource.REMOTE); + + _selectionType = _SelectionType.none; + + return true; + } + + return false; + } + + /// Callback called whenever the person taps on the emoji button in the toolbar. + /// It shows/hides the emoji picker and focus/unfocusses the keyboard accordingly. + void _onEmojiButtonPressed(BuildContext context) { + final isEmojiPickerShown = !_offstageEmojiPickerOffstage; + + // If emoji picker is being shown, we show the keyboard and hide the emoji picker. + if (isEmojiPickerShown) { + _focusNode.requestFocus(); + setState(() { + _offstageEmojiPickerOffstage = true; + }); + } + + // Otherwise, we do the inverse. + else { + // Unfocusing when the person clicks away. This is to hide the keyboard. + // See https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ + // and https://www.youtube.com/watch?v=MKrEJtheGPk&t=40s&ab_channel=HeyFlutter%E2%80%A4com. + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + //currentFocus.unfocus(); + } + + setState(() { + _offstageEmojiPickerOffstage = false; + }); + } + } + /// Renders the image picked by imagePicker from local file storage /// You can also upload the picked image to any server (eg : AWS s3 /// or Firebase) and then return the uploaded image URL. From 4e1df8efe7941427a91001410861b6e860041f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 13:24:17 +0100 Subject: [PATCH 11/43] chore: Renaming emoji picker file. #275 --- .../editor/{emoji_picker_widget.dart => emoji_picker.dart} | 0 lib/presentation/widgets/editor/todo_editor.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/presentation/widgets/editor/{emoji_picker_widget.dart => emoji_picker.dart} (100%) diff --git a/lib/presentation/widgets/editor/emoji_picker_widget.dart b/lib/presentation/widgets/editor/emoji_picker.dart similarity index 100% rename from lib/presentation/widgets/editor/emoji_picker_widget.dart rename to lib/presentation/widgets/editor/emoji_picker.dart diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index d95bd564..1670f1d2 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:ui'; -import 'package:dwyl_app/presentation/widgets/editor/emoji_picker_widget.dart'; +import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; From c18cdcd6decde3c20b7453442bc962d92409ad25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 13:45:33 +0100 Subject: [PATCH 12/43] chore: Separating image callbacks. #275 --- .../widgets/editor/image_callbacks.dart | 74 +++++++++++++++++++ .../widgets/editor/todo_editor.dart | 60 +-------------- 2 files changed, 77 insertions(+), 57 deletions(-) create mode 100644 lib/presentation/widgets/editor/image_callbacks.dart diff --git a/lib/presentation/widgets/editor/image_callbacks.dart b/lib/presentation/widgets/editor/image_callbacks.dart new file mode 100644 index 00000000..21098472 --- /dev/null +++ b/lib/presentation/widgets/editor/image_callbacks.dart @@ -0,0 +1,74 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:http/http.dart' as http; +import 'package:mime/mime.dart'; +import 'package:http_parser/http_parser.dart'; + +/// The URL endpoint in which the images will be uploaded and hosted. +const apiEndpointURL = 'https://imgup.fly.dev/api/images'; + +/// Receives a file [file], copies it to the app's documents directory and returns the path of the copied file. +Future onImagePickCallback(File file) async { + final appDocDir = await getApplicationDocumentsDirectory(); + final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}'); + return copiedFile.path.toString(); +} + +/// Opens gallery (on mobile) or file explorer (on web). +/// Upon picking an image, it is uploaded and the URL of where the image is hosted is returned. +/// +/// Returns `null` if no image was picked or the image was not correctly uploaded. +Future webImagePickImpl(OnImagePickCallback onImagePickCallback) async { + // Lets the user pick one file; files with any file extension can be selected + final result = await ImageFilePicker().pickImage(); + + // The result will be null, if the user aborted the dialog + if (result == null || result.files.isEmpty) { + return null; + } + + // Read file as bytes (https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ#q-how-do-i-access-the-path-on-web) + final platformFile = result.files.first; + final bytes = platformFile.bytes; + + if (bytes == null) { + return null; + } + + // Make HTTP request to upload the image to the file + final request = http.MultipartRequest('POST', Uri.parse(apiEndpointURL)); + + final httpImage = http.MultipartFile.fromBytes( + 'image', + bytes, + contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), + filename: platformFile.name, + ); + request.files.add(httpImage); + + // Check the response and handle accordingly + return http.Client().send(request).then((response) async { + if (response.statusCode != 200) { + return null; + } + + final responseStream = await http.Response.fromStream(response); + final responseData = json.decode(responseStream.body); + return responseData['url']; + }); +} + +// coverage:ignore-start +/// Image file picker wrapper class +class ImageFilePicker { + Future pickImage() => FilePicker.platform.pickFiles(type: FileType.image); +} +// coverage:ignore-end diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index 1670f1d2..d023f178 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -16,6 +16,7 @@ import 'package:http/http.dart' as http; import 'package:mime/mime.dart'; import 'package:http_parser/http_parser.dart'; +import 'image_callbacks.dart'; import 'web_embeds/web_embeds.dart'; const quillEditorKey = Key('quillEditorKey'); @@ -197,10 +198,10 @@ class DeltaTodoEditorState extends State { showImageButton: true, // `onImagePickCallback` is called after image is picked on mobile platforms - onImagePickCallback: _onImagePickCallback, + onImagePickCallback: onImagePickCallback, // `webImagePickImpl` is called after image is picked on the web - webImagePickImpl: _webImagePickImpl, + webImagePickImpl: webImagePickImpl, // defining the selector (we only want to open the gallery whenever the person wants to upload an image) mediaPickSettingSelector: (context) { @@ -364,61 +365,6 @@ class DeltaTodoEditorState extends State { }); } } - - /// Renders the image picked by imagePicker from local file storage - /// You can also upload the picked image to any server (eg : AWS s3 - /// or Firebase) and then return the uploaded image URL. - /// - /// It's only called on mobile platforms. - Future _onImagePickCallback(File file) async { - final appDocDir = await getApplicationDocumentsDirectory(); - final copiedFile = await file.copy('${appDocDir.path}/${basename(file.path)}'); - return copiedFile.path.toString(); - } - - /// Callback that is called after an image is picked whilst on the web platform. - /// Returns the URL of the image. - /// Returns null if an error occurred uploading the file or the image was not picked. - Future _webImagePickImpl(OnImagePickCallback onImagePickCallback) async { - // Lets the user pick one file; files with any file extension can be selected - final result = await ImageFilePicker().pickImage(); - - // The result will be null, if the user aborted the dialog - if (result == null || result.files.isEmpty) { - return null; - } - - // Read file as bytes (https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ#q-how-do-i-access-the-path-on-web) - final platformFile = result.files.first; - final bytes = platformFile.bytes; - - if (bytes == null) { - return null; - } - - // Make HTTP request to upload the image to the file - const apiURL = 'https://imgup.fly.dev/api/images'; - final request = http.MultipartRequest('POST', Uri.parse(apiURL)); - - final httpImage = http.MultipartFile.fromBytes( - 'image', - bytes, - contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), - filename: platformFile.name, - ); - request.files.add(httpImage); - - // Check the response and handle accordingly - return http.Client().send(request).then((response) async { - if (response.statusCode != 200) { - return null; - } - - final responseStream = await http.Response.fromStream(response); - final responseData = json.decode(responseStream.body); - return responseData['url']; - }); - } } // coverage:ignore-start From 5f392303468ab1e9643cef06ef74e8bbf84c0b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 14:23:51 +0100 Subject: [PATCH 13/43] fix: Removing last new line from items. Adding `document` to Item model. #275 --- lib/models/item.dart | 4 ++++ lib/presentation/views/new_todo.dart | 14 +++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/models/item.dart b/lib/models/item.dart index 7fd381a8..9e6e76d9 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -1,3 +1,4 @@ +import 'package:flutter_quill/flutter_quill.dart'; import 'package:uuid/uuid.dart'; // Uuid to generate Ids for the todos @@ -5,15 +6,18 @@ const uuid = Uuid(); /// Todo class. /// Each `Todo` has an `id`, `description` and `completed` boolean field. +/// Optionally, a `document` can be associated to the item. This [Document] is a rich text document. class Item { final String id = uuid.v4(); final String description; final bool completed; + final Document? document; final List _timersList = []; Item({ required this.description, this.completed = false, + this.document }); // Adds a new timer that starts on current time diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 0c3c9d22..7a13193e 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -85,10 +85,18 @@ class _NewTodoPageState extends State { ), ), onPressed: () { - final value = _controller.document.toPlainText(); - if (value.isNotEmpty) { + final document = _controller.document; + var text = document.toPlainText(); + + // Remove last newline from text (document.toPlainText() adds this at the end of the text) + if (text.isNotEmpty) { + final lastChar = text[text.length - 1]; + text = lastChar == '\n' ? text.substring(0, text.length - 1) : text; + } + + if (text.isNotEmpty) { // Create new item and create AddTodo event - final newTodoItem = Item(description: value); + final newTodoItem = Item(description: text, document: document); BlocProvider.of(context).add(AddTodoEvent(newTodoItem)); // Clear textfield From ba0ddcb3a8746157454327268529cf281a090813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 14:24:52 +0100 Subject: [PATCH 14/43] chore: Running format. #275 --- lib/models/item.dart | 2 +- lib/presentation/views/home.dart | 2 +- lib/presentation/views/new_todo.dart | 3 +-- lib/presentation/widgets/editor/image_callbacks.dart | 2 -- lib/presentation/widgets/editor/todo_editor.dart | 7 ------- lib/presentation/widgets/items.dart | 2 +- test/bloc/todo_bloc_test.dart | 4 ++-- 7 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/models/item.dart b/lib/models/item.dart index 9e6e76d9..82ebfc7a 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -17,7 +17,7 @@ class Item { Item({ required this.description, this.completed = false, - this.document + this.document, }); // Adds a new timer that starts on current time diff --git a/lib/presentation/views/home.dart b/lib/presentation/views/home.dart index bedb1035..cec2beff 100644 --- a/lib/presentation/views/home.dart +++ b/lib/presentation/views/home.dart @@ -93,7 +93,7 @@ class HomePage extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: ItemCard(item: items[i]), - ) + ), ], ], ), diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 7a13193e..9b99d80c 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -6,7 +6,6 @@ import 'package:responsive_framework/responsive_framework.dart'; import '../../blocs/blocs.dart'; import '../../models/models.dart'; -import '../widgets/navbar.dart'; const textfieldOnNewPageKey = Key('textfieldOnNewPageKey'); const saveButtonKey = Key('saveButtonKey'); @@ -70,7 +69,7 @@ class _NewTodoPageState extends State { child: DeltaTodoEditor( isWeb: isWeb, editorController: _controller, - )), + ),), // Save button. // When submitted, it adds a new todo item, clears the controller and navigates back diff --git a/lib/presentation/widgets/editor/image_callbacks.dart b/lib/presentation/widgets/editor/image_callbacks.dart index 21098472..bee1e43c 100644 --- a/lib/presentation/widgets/editor/image_callbacks.dart +++ b/lib/presentation/widgets/editor/image_callbacks.dart @@ -1,9 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:ui'; -import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:path/path.dart'; diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index d023f178..77579ecf 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; import 'dart:ui'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; @@ -10,11 +8,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:http/http.dart' as http; -import 'package:mime/mime.dart'; -import 'package:http_parser/http_parser.dart'; import 'image_callbacks.dart'; import 'web_embeds/web_embeds.dart'; diff --git a/lib/presentation/widgets/items.dart b/lib/presentation/widgets/items.dart index f571acf5..b354f6ea 100644 --- a/lib/presentation/widgets/items.dart +++ b/lib/presentation/widgets/items.dart @@ -267,7 +267,7 @@ class _ItemCardState extends State { }()), ), ], - ) + ), ], ), ), diff --git a/test/bloc/todo_bloc_test.dart b/test/bloc/todo_bloc_test.dart index 4f615adb..a3aab1fc 100644 --- a/test/bloc/todo_bloc_test.dart +++ b/test/bloc/todo_bloc_test.dart @@ -24,7 +24,7 @@ void main() { const TodoListLoadedState(items: []), // when the todo bloc was loaded TodoListLoadedState( items: [newItem], - ) // when the todo bloc was added an event + ), // when the todo bloc was added an event ], ); @@ -52,7 +52,7 @@ void main() { expect: () => [ isA(), isA().having((obj) => obj.items.first.completed, 'completed', false), - isA().having((obj) => obj.items.first.completed, 'completed', true) + isA().having((obj) => obj.items.first.completed, 'completed', true), ], ); }); From 4ac50c19b68b8492fe249d9b3ba6534ce33af05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 18:06:15 +0100 Subject: [PATCH 15/43] fix: Fixing tests. #275 --- lib/presentation/views/new_todo.dart | 1 - .../widgets/editor/todo_editor.dart | 9 +- test/widget/widget_test.dart | 97 +++++++------------ 3 files changed, 37 insertions(+), 70 deletions(-) diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 9b99d80c..10746441 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -7,7 +7,6 @@ import 'package:responsive_framework/responsive_framework.dart'; import '../../blocs/blocs.dart'; import '../../models/models.dart'; -const textfieldOnNewPageKey = Key('textfieldOnNewPageKey'); const saveButtonKey = Key('saveButtonKey'); /// Transition handler that navigates the route to the `NewTodo` item page. diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index 77579ecf..c9327219 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -58,6 +58,7 @@ class DeltaTodoEditorState extends State { Widget build(BuildContext context) { // Default editor (for mobile devices) Widget quillEditor = QuillEditor( + key: quillEditorKey, controller: widget.editorController, scrollController: ScrollController(), scrollable: true, @@ -272,7 +273,6 @@ class DeltaTodoEditorState extends State { Expanded( flex: 15, child: Container( - key: quillEditorKey, color: Colors.white, padding: const EdgeInsets.only(left: 16, right: 16), child: quillEditor, @@ -359,10 +359,3 @@ class DeltaTodoEditorState extends State { } } } - -// coverage:ignore-start -/// Image file picker wrapper class -class ImageFilePicker { - Future pickImage() => FilePicker.platform.pickFiles(type: FileType.image); -} -// coverage:ignore-end diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart index b15b2dfb..36b16791 100644 --- a/test/widget/widget_test.dart +++ b/test/widget/widget_test.dart @@ -1,10 +1,17 @@ import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:flutter_quill/flutter_quill_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:dwyl_app/main.dart'; void main() { + + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + testWidgets('Build correctly setup and is loaded', (WidgetTester tester) async { await tester.pumpWidget(const MainApp()); await tester.pump(); @@ -25,29 +32,17 @@ void main() { await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); - // Type text into todo input - await tester.enterText(find.byKey(textfieldOnNewPageKey), 'new todo'); - expect( - find.descendant( - of: find.byKey(textfieldOnNewPageKey), - matching: find.text('new todo'), - ), - findsOneWidget, - ); + final editor = find.byType(QuillEditor); + + // Type text into todo editor + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); // Tap "Save" button to add new todo item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); - // Input is cleared - expect( - find.descendant( - of: find.byKey(textfieldOnNewPageKey), - matching: find.text('new todo'), - ), - findsNothing, - ); - // Pump the widget so it renders the new item await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -72,29 +67,17 @@ void main() { await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); + final editor = find.byType(QuillEditor); + // Type text into todo input - await tester.enterText(find.byKey(textfieldOnNewPageKey), 'new todo'); - expect( - find.descendant( - of: find.byKey(textfieldOnNewPageKey), - matching: find.text('new todo'), - ), - findsOneWidget, - ); + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); // Tap "Save" button to add new todo item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); - // Input is cleared - expect( - find.descendant( - of: find.byKey(textfieldOnNewPageKey), - matching: find.text('new todo'), - ), - findsNothing, - ); - // Pump the widget so it renders the new item await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -108,7 +91,7 @@ void main() { testWidgets('Adding a new todo item shows a card (on tablet screen)', (WidgetTester tester) async { // Ensure binding is initialized to setup camera size TestWidgetsFlutterBinding.ensureInitialized(); - tester.view.physicalSize = const Size(400, 600); + tester.view.physicalSize = const Size(400, 900); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MainApp()); @@ -122,29 +105,17 @@ void main() { await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); + final editor = find.byType(QuillEditor); + // Type text into todo input - await tester.enterText(find.byKey(textfieldOnNewPageKey), 'new todo'); - expect( - find.descendant( - of: find.byKey(textfieldOnNewPageKey), - matching: find.text('new todo'), - ), - findsOneWidget, - ); + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); // Tap "Save" button to add new todo item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); - // Input is cleared - expect( - find.descendant( - of: find.byKey(textfieldOnNewPageKey), - matching: find.text('new todo'), - ), - findsNothing, - ); - // Pump the widget so it renders the new item await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -168,11 +139,13 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 2)); // Type text into todo input and tap "Save" button to add new todo item - await tester.enterText(find.byKey(textfieldOnNewPageKey), 'new todo'); - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); + final editor = find.byType(QuillEditor); - // Pump the widget so it renders the new item + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); // Expect to find at least one widget, pertaining to the one that was added @@ -209,11 +182,13 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 2)); // Type text into todo input and tap "Save" button to add new todo item - await tester.enterText(find.byKey(textfieldOnNewPageKey), 'new todo'); - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); + final editor = find.byType(QuillEditor); - // Pump the widget so it renders the new item + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); // Expect to find at least one widget, pertaining to the one that was added From 2e31b996b1dfc5041b407f8882222def9b767c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 18:23:27 +0100 Subject: [PATCH 16/43] chore: Refactoring name of folder. #275 --- lib/blocs/{cubit => app}/app_cubit.dart | 0 lib/blocs/{cubit => app}/app_state.dart | 0 lib/blocs/blocs.dart | 1 + lib/main.dart | 1 - lib/presentation/views/new_todo.dart | 1 - lib/presentation/widgets/editor/web_embeds/web_embeds.dart | 3 +-- test/bloc/{ => todo}/todo_bloc_test.dart | 0 test/bloc/{ => todo}/todo_event_test.dart | 0 test/bloc/{ => todo}/todo_state_test.dart | 0 9 files changed, 2 insertions(+), 4 deletions(-) rename lib/blocs/{cubit => app}/app_cubit.dart (100%) rename lib/blocs/{cubit => app}/app_state.dart (100%) rename test/bloc/{ => todo}/todo_bloc_test.dart (100%) rename test/bloc/{ => todo}/todo_event_test.dart (100%) rename test/bloc/{ => todo}/todo_state_test.dart (100%) diff --git a/lib/blocs/cubit/app_cubit.dart b/lib/blocs/app/app_cubit.dart similarity index 100% rename from lib/blocs/cubit/app_cubit.dart rename to lib/blocs/app/app_cubit.dart diff --git a/lib/blocs/cubit/app_state.dart b/lib/blocs/app/app_state.dart similarity index 100% rename from lib/blocs/cubit/app_state.dart rename to lib/blocs/app/app_state.dart diff --git a/lib/blocs/blocs.dart b/lib/blocs/blocs.dart index 028a55da..e9c71a18 100644 --- a/lib/blocs/blocs.dart +++ b/lib/blocs/blocs.dart @@ -1,3 +1,4 @@ export 'package:flutter_bloc/flutter_bloc.dart'; export 'todo/todo_bloc.dart'; +export 'app/app_cubit.dart'; diff --git a/lib/main.dart b/lib/main.dart index d75d72f1..b434eb33 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; import 'package:dwyl_app/logging/logging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_todo.dart index 10746441..abfcb704 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_todo.dart @@ -1,4 +1,3 @@ -import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; diff --git a/lib/presentation/widgets/editor/web_embeds/web_embeds.dart b/lib/presentation/widgets/editor/web_embeds/web_embeds.dart index d88c56c7..4bc5917a 100644 --- a/lib/presentation/widgets/editor/web_embeds/web_embeds.dart +++ b/lib/presentation/widgets/editor/web_embeds/web_embeds.dart @@ -1,12 +1,11 @@ -import 'package:dwyl_app/blocs/cubit/app_cubit.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:universal_html/html.dart' as html; // Conditionally importing the PlatformViewRegistry class according to the platform +import '../../../../blocs/blocs.dart'; import 'mobile_platform_registry.dart' if (dart.library.html) 'web_platform_registry.dart' as ui_instance; /// Class used to conditionally register the view factory. diff --git a/test/bloc/todo_bloc_test.dart b/test/bloc/todo/todo_bloc_test.dart similarity index 100% rename from test/bloc/todo_bloc_test.dart rename to test/bloc/todo/todo_bloc_test.dart diff --git a/test/bloc/todo_event_test.dart b/test/bloc/todo/todo_event_test.dart similarity index 100% rename from test/bloc/todo_event_test.dart rename to test/bloc/todo/todo_event_test.dart diff --git a/test/bloc/todo_state_test.dart b/test/bloc/todo/todo_state_test.dart similarity index 100% rename from test/bloc/todo_state_test.dart rename to test/bloc/todo/todo_state_test.dart From d64f9aa6b0641da3b85174c3fc31811d31ccf1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 18:33:27 +0100 Subject: [PATCH 17/43] fix: Testing cubit. #275 --- lib/blocs/app/app_state.dart | 6 +++--- test/bloc/app/app_cubit_test.dart | 13 +++++++++++++ test/bloc/app/app_state_test.dart | 12 ++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 test/bloc/app/app_cubit_test.dart create mode 100644 test/bloc/app/app_state_test.dart diff --git a/lib/blocs/app/app_state.dart b/lib/blocs/app/app_state.dart index 24b7ccc3..00597024 100644 --- a/lib/blocs/app/app_state.dart +++ b/lib/blocs/app/app_state.dart @@ -4,11 +4,11 @@ sealed class AppState extends Equatable { final bool isWeb; const AppState({required this.isWeb}); - - @override - List get props => []; } final class AppInitial extends AppState { const AppInitial({required super.isWeb}); + + @override + List get props => []; } diff --git a/test/bloc/app/app_cubit_test.dart b/test/bloc/app/app_cubit_test.dart new file mode 100644 index 00000000..c664d808 --- /dev/null +++ b/test/bloc/app/app_cubit_test.dart @@ -0,0 +1,13 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppCubit', () { + blocTest( + 'emits [] on initial setup', + build: () => AppCubit(isWeb: true), + expect: () => [], + ); + }); +} diff --git a/test/bloc/app/app_state_test.dart b/test/bloc/app/app_state_test.dart new file mode 100644 index 00000000..2048ccb7 --- /dev/null +++ b/test/bloc/app/app_state_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:dwyl_app/blocs/blocs.dart'; + +void main() { + group('AppCubit', () { + group('AppInitial', () { + test('supports value comparison', () { + expect(const AppInitial(isWeb: true).props, const AppInitial(isWeb: true).props); + }); + }); + }); +} From ff5d21f978c04a0109b592a566de6d22c771aa8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 18:46:40 +0100 Subject: [PATCH 18/43] fix: Providing blocks on Main so we can test different scenarios in test widgets. #275 --- lib/main.dart | 46 ++++++++++++----------- test/widget/widget_test.dart | 71 ++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 29 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b434eb33..88b7aec6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,19 @@ import 'presentation/views/views.dart'; // coverage:ignore-start void main() { - runApp(const MainApp()); + // Setting global log bloc observer and Lumberdash + Bloc.observer = GlobalLogBlocObserver(); + putLumberdashToWork(withClients: [ColorizeLumberdash()]); + + runApp( + MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: kIsWeb)), + ], + child: const MainApp(), + ), + ); } // coverage:ignore-end @@ -22,27 +34,17 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - // Setting global log bloc observer and Lumberdash - Bloc.observer = GlobalLogBlocObserver(); - putLumberdashToWork(withClients: [ColorizeLumberdash()]); - - return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), - BlocProvider(create: (context) => AppCubit(isWeb: kIsWeb)), - ], - child: MaterialApp( - home: const HomePage(), - builder: (context, child) => ResponsiveBreakpoints.builder( - child: child!, - breakpoints: [ - const Breakpoint(start: 0, end: 425, name: MOBILE), - const Breakpoint(start: 426, end: 768, name: TABLET), - const Breakpoint(start: 769, end: 1024, name: DESKTOP), - const Breakpoint(start: 1025, end: 1440, name: 'LARGE_DESKTOP'), - const Breakpoint(start: 1441, end: double.infinity, name: '4K'), - ], - ), + return MaterialApp( + home: const HomePage(), + builder: (context, child) => ResponsiveBreakpoints.builder( + child: child!, + breakpoints: [ + const Breakpoint(start: 0, end: 425, name: MOBILE), + const Breakpoint(start: 426, end: 768, name: TABLET), + const Breakpoint(start: 769, end: 1024, name: DESKTOP), + const Breakpoint(start: 1025, end: 1440, name: 'LARGE_DESKTOP'), + const Breakpoint(start: 1441, end: double.infinity, name: '4K'), + ], ), ); } diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart index 36b16791..2a5c40a3 100644 --- a/test/widget/widget_test.dart +++ b/test/widget/widget_test.dart @@ -1,3 +1,4 @@ +import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; @@ -7,13 +8,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:dwyl_app/main.dart'; void main() { + /// Bootstraps a sample main application, whether it [isWeb] or not. + Widget initializeMainApp({required bool isWeb}) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), + ], + child: const MainApp(), + ); + } setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); testWidgets('Build correctly setup and is loaded', (WidgetTester tester) async { - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); await tester.pump(); // Find the text input and string stating 0 todos created @@ -21,7 +33,8 @@ void main() { }); testWidgets('Adding a new todo item shows a card', (WidgetTester tester) async { - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); await tester.pumpAndSettle(); // Find the text input and string stating 0 todos created @@ -56,7 +69,8 @@ void main() { tester.view.physicalSize = const Size(400, 600); tester.view.devicePixelRatio = 1.0; - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); await tester.pumpAndSettle(); // Find the text input and string stating 0 todos created @@ -94,7 +108,47 @@ void main() { tester.view.physicalSize = const Size(400, 900); tester.view.devicePixelRatio = 1.0; - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + final editor = find.byType(QuillEditor); + + // Type text into todo input + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + // Tap "Save" button to add new todo item + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Pump the widget so it renders the new item + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + + // Resetting camera size to normal + addTearDown(tester.view.resetPhysicalSize); + }); + + testWidgets('Adding a new todo item shows a card (on web platform)', (WidgetTester tester) async { + // Ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(400, 600); + tester.view.devicePixelRatio = 1.0; + + final app = initializeMainApp(isWeb: true); + await tester.pumpWidget(app); await tester.pumpAndSettle(); // Find the text input and string stating 0 todos created @@ -127,7 +181,8 @@ void main() { }); testWidgets('Adding a new todo item and checking it as done', (WidgetTester tester) async { - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); await tester.pumpAndSettle(); // Find the text input and string stating 0 todos created @@ -170,7 +225,8 @@ void main() { }); testWidgets('Adding a new todo item and clicking timer button and marking it as done while it\'s running', (WidgetTester tester) async { - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); await tester.pumpAndSettle(); // Find the text input and string stating 0 todos created @@ -246,7 +302,8 @@ void main() { }); testWidgets('Navigate to new page and go back', (WidgetTester tester) async { - await tester.pumpWidget(const MainApp()); + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); await tester.pumpAndSettle(); // Find the text input and string stating 0 todos created From 3510c4939c81871dbcd18213f083c9abe3bfd1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 19:06:48 +0100 Subject: [PATCH 19/43] fix: Add widget test. #275 --- test/widget/emoji_widget_test.dart | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 test/widget/emoji_widget_test.dart diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart new file mode 100644 index 00000000..e6543baa --- /dev/null +++ b/test/widget/emoji_widget_test.dart @@ -0,0 +1,85 @@ + +import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/main.dart'; +import 'package:dwyl_app/presentation/views/views.dart'; +import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; +import 'package:dwyl_app/presentation/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_test/flutter_test.dart'; + + + +void main() { + + /// Bootstraps a sample main application, whether it [isWeb] or not. + Widget initializeMainApp({required bool isWeb}) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), + ], + child: const MainApp(), + ); + } + + /// Check for context: https://stackoverflow.com/questions/60671728/unable-to-load-assets-in-flutter-tests + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + testWidgets('Click on emoji button should show the emoji picker', (WidgetTester tester) async { + // Set size because it's needed to correctly tap on emoji picker + // and ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(380, 800); + tester.view.devicePixelRatio = 1.0; + await tester.binding.setSurfaceSize(const Size(380, 800)); + + // Initialize app + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + + // Expect to find the normal page setup and emoji picker not being shown + final editor = find.byType(QuillEditor); + expect(editor.hitTestable(), findsOneWidget); + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + + // Click on emoji button should show the emoji picker + var emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + emojiIcon = find.byIcon(Icons.emoji_emotions); + + // Expect the emoji picker being shown + expect(find.byKey(emojiButtonKey).hitTestable(), findsOneWidget); + + // Tap on smile category + await tester.tapAt(const Offset(61, 580)); + await tester.pumpAndSettle(); + + // Tap on smile icon + await tester.tapAt(const Offset(14, 632)); + await tester.pumpAndSettle(); + + // Tap on emoji icon to close the emoji pickers + emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + }); +} From 33fb731c30cc1ee5a5604e0e2e92b32419470ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 19:12:36 +0100 Subject: [PATCH 20/43] chore: Removing code that's not being used. #275 --- .../widgets/editor/todo_editor.dart | 47 ------------------- test/widget/widget_test.dart | 1 + 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index c9327219..ba372692 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -77,9 +77,6 @@ class DeltaTodoEditorState extends State { }); return false; }, - onTapUp: (details, p1) { - return _onTripleClickSelection(); - }, customStyles: DefaultStyles( h1: DefaultTextBlockStyle( const TextStyle( @@ -139,9 +136,6 @@ class DeltaTodoEditorState extends State { placeholder: 'Add content', expands: false, padding: const EdgeInsets.only(top: 16.0), - onTapUp: (details, p1) { - return _onTripleClickSelection(); - }, customStyles: DefaultStyles( h1: DefaultTextBlockStyle( const TextStyle( @@ -288,47 +282,6 @@ class DeltaTodoEditorState extends State { ); } - /// Callback called whenever the person taps on the text. - /// It will select nothing, then the word if another tap is detected - /// and then the whole text if another tap is detected (triple). - bool _onTripleClickSelection() { - final controller = widget.editorController; - - // If nothing is selected, selection type is `none` - if (controller.selection.isCollapsed) { - _selectionType = _SelectionType.none; - } - - // If nothing is selected, selection type becomes `word - if (_selectionType == _SelectionType.none) { - _selectionType = _SelectionType.word; - return false; - } - - // If the word is selected, select all text - if (_selectionType == _SelectionType.word) { - final child = controller.document.queryChild( - controller.selection.baseOffset, - ); - final offset = child.node?.documentOffset ?? 0; - final length = child.node?.length ?? 0; - - final selection = TextSelection( - baseOffset: offset, - extentOffset: offset + length, - ); - - // Select all text and make next selection to `none` - controller.updateSelection(selection, ChangeSource.REMOTE); - - _selectionType = _SelectionType.none; - - return true; - } - - return false; - } - /// Callback called whenever the person taps on the emoji button in the toolbar. /// It shows/hides the emoji picker and focus/unfocusses the keyboard accordingly. void _onEmojiButtonPressed(BuildContext context) { diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart index 2a5c40a3..c587c32b 100644 --- a/test/widget/widget_test.dart +++ b/test/widget/widget_test.dart @@ -1,6 +1,7 @@ import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill_test.dart'; From f1dc9b825aed6f2b4a88b46f51580c4c34bb9eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 19:15:32 +0100 Subject: [PATCH 21/43] fix: Run format. #275 --- lib/presentation/widgets/editor/todo_editor.dart | 3 +-- test/widget/widget_test.dart | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index ba372692..d5f728ad 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:ui'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/extensions.dart'; @@ -44,7 +43,7 @@ class DeltaTodoEditorState extends State { final FocusNode _focusNode = FocusNode(); /// Selection types for triple clicking - _SelectionType _selectionType = _SelectionType.none; + final _SelectionType _selectionType = _SelectionType.none; /// Show emoji picker bool _offstageEmojiPickerOffstage = true; diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart index c587c32b..2a5c40a3 100644 --- a/test/widget/widget_test.dart +++ b/test/widget/widget_test.dart @@ -1,7 +1,6 @@ import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill_test.dart'; From ce852f159789228597aa1bea7f853352527af2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 2 Oct 2023 19:21:51 +0100 Subject: [PATCH 22/43] chore: Removing dead code. #275 --- lib/presentation/widgets/editor/todo_editor.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/todo_editor.dart index d5f728ad..f84c3ab8 100644 --- a/lib/presentation/widgets/editor/todo_editor.dart +++ b/lib/presentation/widgets/editor/todo_editor.dart @@ -14,12 +14,6 @@ import 'web_embeds/web_embeds.dart'; const quillEditorKey = Key('quillEditorKey'); const emojiButtonKey = Key('emojiButtonKey'); -/// Types of selection that person can make when triple clicking -enum _SelectionType { - none, - word, -} - /// Home page with the `flutter-quill` editor class DeltaTodoEditor extends StatefulWidget { /// Is the platform web-based? @@ -42,9 +36,6 @@ class DeltaTodoEditorState extends State { /// Focus node used to obtain keyboard focus and events final FocusNode _focusNode = FocusNode(); - /// Selection types for triple clicking - final _SelectionType _selectionType = _SelectionType.none; - /// Show emoji picker bool _offstageEmojiPickerOffstage = true; From 99ac1e760ac6c7c94edc9cf9224c34f8a869a69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 10:24:36 +0100 Subject: [PATCH 23/43] chore: Refactoring and organizing tests. #275 --- test/integration_test.dart | 404 +++++++++++++++++++++++++++++ test/widget/emoji_widget_test.dart | 53 ---- test/widget/widget_test.dart | 337 ------------------------ 3 files changed, 404 insertions(+), 390 deletions(-) create mode 100644 test/integration_test.dart delete mode 100644 test/widget/widget_test.dart diff --git a/test/integration_test.dart b/test/integration_test.dart new file mode 100644 index 00000000..ddf607db --- /dev/null +++ b/test/integration_test.dart @@ -0,0 +1,404 @@ +import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/presentation/views/views.dart'; +import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; +import 'package:dwyl_app/presentation/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:dwyl_app/main.dart'; + +void main() { + /// Bootstraps a sample main application, whether it [isWeb] or not. + Widget initializeMainApp({required bool isWeb}) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), + ], + child: const MainApp(), + ); + } + + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('Normal build', () { + testWidgets('is correctly setup and loaded.', (WidgetTester tester) async { + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pump(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + }); + }); + + group('Adding a new todo item', () { + testWidgets('shows a card', (WidgetTester tester) async { + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + final editor = find.byType(QuillEditor); + + // Type text into todo editor + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + // Tap "Save" button to add new todo item + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Pump the widget so it renders the new item + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + }); + + testWidgets('shows a card (on mobile screen).', (WidgetTester tester) async { + // Ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(400, 600); + tester.view.devicePixelRatio = 1.0; + + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + final editor = find.byType(QuillEditor); + + // Lose focus + // await tester.tap(find.byKey(navBarInNewTodoPageKey)); + // await tester.pumpAndSettle(); + + // Type text into todo input + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + // Tap "Save" button to add new todo item + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Pump the widget so it renders the new item + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + + // Resetting camera size to normal + addTearDown(tester.view.resetPhysicalSize); + }); + + testWidgets('shows a card (on tablet screen).', (WidgetTester tester) async { + // Ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(400, 900); + tester.view.devicePixelRatio = 1.0; + + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + final editor = find.byType(QuillEditor); + + // Type text into todo input + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + // Tap "Save" button to add new todo item + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Pump the widget so it renders the new item + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + + // Resetting camera size to normal + addTearDown(tester.view.resetPhysicalSize); + }); + + testWidgets('shows a card (on web platform).', (WidgetTester tester) async { + // Ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(400, 600); + tester.view.devicePixelRatio = 1.0; + + final app = initializeMainApp(isWeb: true); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + final editor = find.byType(QuillEditor); + + // Type text into todo input + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + // Tap "Save" button to add new todo item + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Pump the widget so it renders the new item + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + + // Resetting camera size to normal + addTearDown(tester.view.resetPhysicalSize); + }); + + testWidgets('and checking it as done.', (WidgetTester tester) async { + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Type text into todo input and tap "Save" button to add new todo item + final editor = find.byType(QuillEditor); + + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + + // Getting widget to test its value + final checkboxFinder = find.descendant( + of: find.byKey(itemCardWidgetKey), + matching: find.byType(Icon), + ); + var checkboxWidget = tester.firstWidget(checkboxFinder); + + expect(checkboxWidget.icon, Icons.check_box_outline_blank); + + // Tap on item card + await tester.tap(find.byKey(itemCardWidgetKey)); + await tester.pump(const Duration(seconds: 2)); + + // Updating item card widget and checkbox value should be true + checkboxWidget = tester.firstWidget(checkboxFinder); + expect(checkboxWidget.icon, Icons.check_box); + }); + + testWidgets('and clicking timer button and marking it as done while it\'s running.', (WidgetTester tester) async { + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Type text into todo input and tap "Save" button to add new todo item + final editor = find.byType(QuillEditor); + + await tester.tap(editor); + await tester.quillEnterText(editor, 'new todo\n'); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(saveButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find at least one widget, pertaining to the one that was added + expect(find.byKey(itemCardWidgetKey), findsOneWidget); + + // Getting widget to test its value + var buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); + + // Button should be stopped + var buttonText = buttonWidget.child as Text; + expect(buttonText.data, 'Start'); + + // Tap on timer button. + await tester.tap(find.byKey(itemCardTimerButtonKey)); + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + // Updating widget and button should be ongoing + buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); + buttonText = buttonWidget.child as Text; + expect(buttonText.data, 'Stop'); + + // Tap on timer button AGAIN + await tester.tap(find.byKey(itemCardTimerButtonKey)); + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + // Updating widget and button should be stopped + buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); + buttonText = buttonWidget.child as Text; + expect(buttonText.data, 'Resume'); + + // Tap on timer button AGAIN x2 + await tester.tap(find.byKey(itemCardTimerButtonKey)); + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + // Updating widget and button should be ongoing + buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); + buttonText = buttonWidget.child as Text; + expect(buttonText.data, 'Stop'); + + // Tap on item card while its ongoing + await tester.tap(find.byKey(itemCardWidgetKey)); + await tester.pumpAndSettle(); + + // Item card should be marked as done + final checkboxFinder = find.descendant( + of: find.byKey(itemCardWidgetKey), + matching: find.byType(Icon), + ); + var checkboxWidget = tester.firstWidget(checkboxFinder); + checkboxWidget = tester.firstWidget(checkboxFinder); + expect(checkboxWidget.icon, Icons.check_box); + }); + }); + + group('Emoji picker', () { + testWidgets('should shown when clicking in the emoji button.', (WidgetTester tester) async { + // Set size because it's needed to correctly tap on emoji picker + // and ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(380, 800); + tester.view.devicePixelRatio = 1.0; + await tester.binding.setSurfaceSize(const Size(380, 800)); + + // Initialize app + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find the normal page setup and emoji picker not being shown + final editor = find.byType(QuillEditor); + expect(editor.hitTestable(), findsOneWidget); + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + + // Click on emoji button should show the emoji picker + var emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + emojiIcon = find.byIcon(Icons.emoji_emotions); + + // Expect the emoji picker being shown + expect(find.byKey(emojiButtonKey).hitTestable(), findsOneWidget); + + // Tap on smile category + await tester.tapAt(const Offset(61, 580)); + await tester.pumpAndSettle(); + + // Tap on smile icon + await tester.tapAt(const Offset(14, 632)); + await tester.pumpAndSettle(); + + // Tap on emoji icon to close the emoji pickers + emojiIcon = find.byIcon(Icons.emoji_emotions); + + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + }); + }); + + group('Navigation', () { + testWidgets('from the new item page and go back.', (WidgetTester tester) async { + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 todos created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Go back to the page + expect(find.byKey(textfieldKey), findsNothing); + + await tester.tap(find.byKey(backButtonKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // User went back to the home page + expect(find.byKey(textfieldKey), findsOneWidget); + + // Tap textfield again to open new page to create todo item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Tap on the logo icon. Person should go back. + await tester.tap(find.byKey(logoKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // User went back to the home page + expect(find.byKey(textfieldKey), findsOneWidget); + }); + }); +} diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart index e6543baa..8ab6c780 100644 --- a/test/widget/emoji_widget_test.dart +++ b/test/widget/emoji_widget_test.dart @@ -28,58 +28,5 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); }); - testWidgets('Click on emoji button should show the emoji picker', (WidgetTester tester) async { - // Set size because it's needed to correctly tap on emoji picker - // and ensure binding is initialized to setup camera size - TestWidgetsFlutterBinding.ensureInitialized(); - tester.view.physicalSize = const Size(380, 800); - tester.view.devicePixelRatio = 1.0; - await tester.binding.setSurfaceSize(const Size(380, 800)); - - // Initialize app - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - - // Expect to find the normal page setup and emoji picker not being shown - final editor = find.byType(QuillEditor); - expect(editor.hitTestable(), findsOneWidget); - expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); - - // Click on emoji button should show the emoji picker - var emojiIcon = find.byIcon(Icons.emoji_emotions); - - await tester.tap(emojiIcon); - await tester.pumpAndSettle(); - emojiIcon = find.byIcon(Icons.emoji_emotions); - - // Expect the emoji picker being shown - expect(find.byKey(emojiButtonKey).hitTestable(), findsOneWidget); - - // Tap on smile category - await tester.tapAt(const Offset(61, 580)); - await tester.pumpAndSettle(); - - // Tap on smile icon - await tester.tapAt(const Offset(14, 632)); - await tester.pumpAndSettle(); - - // Tap on emoji icon to close the emoji pickers - emojiIcon = find.byIcon(Icons.emoji_emotions); - - await tester.tap(emojiIcon); - await tester.pumpAndSettle(); - - expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); - }); } diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart deleted file mode 100644 index 2a5c40a3..00000000 --- a/test/widget/widget_test.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'package:dwyl_app/blocs/blocs.dart'; -import 'package:dwyl_app/presentation/views/views.dart'; -import 'package:dwyl_app/presentation/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart' hide Text; -import 'package:flutter_quill/flutter_quill_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:dwyl_app/main.dart'; - -void main() { - /// Bootstraps a sample main application, whether it [isWeb] or not. - Widget initializeMainApp({required bool isWeb}) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), - BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), - ], - child: const MainApp(), - ); - } - - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - testWidgets('Build correctly setup and is loaded', (WidgetTester tester) async { - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pump(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - }); - - testWidgets('Adding a new todo item shows a card', (WidgetTester tester) async { - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - final editor = find.byType(QuillEditor); - - // Type text into todo editor - await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); - await tester.pumpAndSettle(); - - // Tap "Save" button to add new todo item - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Pump the widget so it renders the new item - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Expect to find at least one widget, pertaining to the one that was added - expect(find.byKey(itemCardWidgetKey), findsOneWidget); - }); - - testWidgets('Adding a new todo item shows a card (on mobile screen)', (WidgetTester tester) async { - // Ensure binding is initialized to setup camera size - TestWidgetsFlutterBinding.ensureInitialized(); - tester.view.physicalSize = const Size(400, 600); - tester.view.devicePixelRatio = 1.0; - - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - final editor = find.byType(QuillEditor); - - // Type text into todo input - await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); - await tester.pumpAndSettle(); - - // Tap "Save" button to add new todo item - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Pump the widget so it renders the new item - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Expect to find at least one widget, pertaining to the one that was added - expect(find.byKey(itemCardWidgetKey), findsOneWidget); - - // Resetting camera size to normal - addTearDown(tester.view.resetPhysicalSize); - }); - - testWidgets('Adding a new todo item shows a card (on tablet screen)', (WidgetTester tester) async { - // Ensure binding is initialized to setup camera size - TestWidgetsFlutterBinding.ensureInitialized(); - tester.view.physicalSize = const Size(400, 900); - tester.view.devicePixelRatio = 1.0; - - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - final editor = find.byType(QuillEditor); - - // Type text into todo input - await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); - await tester.pumpAndSettle(); - - // Tap "Save" button to add new todo item - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Pump the widget so it renders the new item - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Expect to find at least one widget, pertaining to the one that was added - expect(find.byKey(itemCardWidgetKey), findsOneWidget); - - // Resetting camera size to normal - addTearDown(tester.view.resetPhysicalSize); - }); - - testWidgets('Adding a new todo item shows a card (on web platform)', (WidgetTester tester) async { - // Ensure binding is initialized to setup camera size - TestWidgetsFlutterBinding.ensureInitialized(); - tester.view.physicalSize = const Size(400, 600); - tester.view.devicePixelRatio = 1.0; - - final app = initializeMainApp(isWeb: true); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - final editor = find.byType(QuillEditor); - - // Type text into todo input - await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); - await tester.pumpAndSettle(); - - // Tap "Save" button to add new todo item - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Pump the widget so it renders the new item - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Expect to find at least one widget, pertaining to the one that was added - expect(find.byKey(itemCardWidgetKey), findsOneWidget); - - // Resetting camera size to normal - addTearDown(tester.view.resetPhysicalSize); - }); - - testWidgets('Adding a new todo item and checking it as done', (WidgetTester tester) async { - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Type text into todo input and tap "Save" button to add new todo item - final editor = find.byType(QuillEditor); - - await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Expect to find at least one widget, pertaining to the one that was added - expect(find.byKey(itemCardWidgetKey), findsOneWidget); - - // Getting widget to test its value - final checkboxFinder = find.descendant( - of: find.byKey(itemCardWidgetKey), - matching: find.byType(Icon), - ); - var checkboxWidget = tester.firstWidget(checkboxFinder); - - expect(checkboxWidget.icon, Icons.check_box_outline_blank); - - // Tap on item card - await tester.tap(find.byKey(itemCardWidgetKey)); - await tester.pump(const Duration(seconds: 2)); - - // Updating item card widget and checkbox value should be true - checkboxWidget = tester.firstWidget(checkboxFinder); - expect(checkboxWidget.icon, Icons.check_box); - }); - - testWidgets('Adding a new todo item and clicking timer button and marking it as done while it\'s running', (WidgetTester tester) async { - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Type text into todo input and tap "Save" button to add new todo item - final editor = find.byType(QuillEditor); - - await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(saveButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Expect to find at least one widget, pertaining to the one that was added - expect(find.byKey(itemCardWidgetKey), findsOneWidget); - - // Getting widget to test its value - var buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); - - // Button should be stopped - var buttonText = buttonWidget.child as Text; - expect(buttonText.data, 'Start'); - - // Tap on timer button. - await tester.tap(find.byKey(itemCardTimerButtonKey)); - await tester.pump(const Duration(seconds: 2)); - await tester.pumpAndSettle(); - - // Updating widget and button should be ongoing - buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); - buttonText = buttonWidget.child as Text; - expect(buttonText.data, 'Stop'); - - // Tap on timer button AGAIN - await tester.tap(find.byKey(itemCardTimerButtonKey)); - await tester.pump(const Duration(seconds: 2)); - await tester.pumpAndSettle(); - - // Updating widget and button should be stopped - buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); - buttonText = buttonWidget.child as Text; - expect(buttonText.data, 'Resume'); - - // Tap on timer button AGAIN x2 - await tester.tap(find.byKey(itemCardTimerButtonKey)); - await tester.pump(const Duration(seconds: 2)); - await tester.pumpAndSettle(); - - // Updating widget and button should be ongoing - buttonWidget = tester.firstWidget(find.byKey(itemCardTimerButtonKey)); - buttonText = buttonWidget.child as Text; - expect(buttonText.data, 'Stop'); - - // Tap on item card while its ongoing - await tester.tap(find.byKey(itemCardWidgetKey)); - await tester.pumpAndSettle(); - - // Item card should be marked as done - final checkboxFinder = find.descendant( - of: find.byKey(itemCardWidgetKey), - matching: find.byType(Icon), - ); - var checkboxWidget = tester.firstWidget(checkboxFinder); - checkboxWidget = tester.firstWidget(checkboxFinder); - expect(checkboxWidget.icon, Icons.check_box); - }); - - testWidgets('Navigate to new page and go back', (WidgetTester tester) async { - final app = initializeMainApp(isWeb: false); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - - // Find the text input and string stating 0 todos created - expect(find.byKey(textfieldKey), findsOneWidget); - expect(find.byKey(itemCardWidgetKey), findsNothing); - - // Tap textfield to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Go back to the page - expect(find.byKey(textfieldKey), findsNothing); - - await tester.tap(find.byKey(backButtonKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // User went back to the home page - expect(find.byKey(textfieldKey), findsOneWidget); - - // Tap textfield again to open new page to create todo item - await tester.tap(find.byKey(textfieldKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Tap on the logo icon. Person should go back. - await tester.tap(find.byKey(logoKey)); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // User went back to the home page - expect(find.byKey(textfieldKey), findsOneWidget); - }); -} From 2a7fe42a0d86bf33240c8035fdb1647c9f081b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 10:37:57 +0100 Subject: [PATCH 24/43] chore: Renaming `Todo` to `Item`. #275 --- lib/blocs/blocs.dart | 2 +- lib/blocs/item/item_bloc.dart | 77 ++++++++++++++++++ lib/blocs/item/item_event.dart | 41 ++++++++++ lib/blocs/item/item_state.dart | 25 ++++++ lib/blocs/todo/todo_bloc.dart | 78 ------------------- lib/blocs/todo/todo_event.dart | 41 ---------- lib/blocs/todo/todo_state.dart | 25 ------ lib/main.dart | 4 +- lib/models/item.dart | 6 +- lib/presentation/views/home.dart | 16 ++-- .../views/{new_todo.dart => new_item.dart} | 27 +++---- lib/presentation/views/views.dart | 2 +- .../{todo_editor.dart => item_editor.dart} | 0 lib/presentation/widgets/items.dart | 37 +++------ lib/presentation/widgets/widgets.dart | 2 +- test/bloc/log_bloc_observer_test.dart | 9 +-- test/bloc/todo/todo_bloc_test.dart | 48 ++++++------ test/bloc/todo/todo_event_test.dart | 18 ++--- test/bloc/todo/todo_state_test.dart | 14 ++-- test/integration_test.dart | 72 ++++++++--------- test/widget/emoji_widget_test.dart | 8 +- web/manifest.json | 4 +- 22 files changed, 267 insertions(+), 289 deletions(-) create mode 100644 lib/blocs/item/item_bloc.dart create mode 100644 lib/blocs/item/item_event.dart create mode 100644 lib/blocs/item/item_state.dart delete mode 100644 lib/blocs/todo/todo_bloc.dart delete mode 100644 lib/blocs/todo/todo_event.dart delete mode 100644 lib/blocs/todo/todo_state.dart rename lib/presentation/views/{new_todo.dart => new_item.dart} (85%) rename lib/presentation/widgets/editor/{todo_editor.dart => item_editor.dart} (100%) diff --git a/lib/blocs/blocs.dart b/lib/blocs/blocs.dart index e9c71a18..0f906985 100644 --- a/lib/blocs/blocs.dart +++ b/lib/blocs/blocs.dart @@ -1,4 +1,4 @@ export 'package:flutter_bloc/flutter_bloc.dart'; -export 'todo/todo_bloc.dart'; +export 'item/item_bloc.dart'; export 'app/app_cubit.dart'; diff --git a/lib/blocs/item/item_bloc.dart b/lib/blocs/item/item_bloc.dart new file mode 100644 index 00000000..6b3f92c1 --- /dev/null +++ b/lib/blocs/item/item_bloc.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../models/item.dart'; + +part 'item_event.dart'; +part 'item_state.dart'; + +/// This is the ItemBloc, +/// the bloc that manages the list of items. +class ItemBloc extends Bloc { + // Bloc constructor and adding event handlers + ItemBloc() : super(ItemInitialState()) { + on(_onStart); + on(_addItem); + on(_removeItem); + on(_toggleItem); + } + + void _onStart(ItemListStarted event, Emitter emit) { + // You could do stuff here like: + // 1. emit "loading state" + // 2. fetch items from API + // 3. emit "success state" or "error state" + + emit(const ItemListLoadedState(items: [])); + } + + // AddItem event handler which emits ItemAdded state + void _addItem(AddItemEvent event, Emitter emit) { + final state = this.state; + + // Check if list is loaded + if (state is ItemListLoadedState) { + emit(ItemListLoadedState(items: [...state.items, event.itemObj])); + } + } + + // RemoveItem event handler which emits ItemDeleted state + void _removeItem(RemoveItemEvent event, Emitter emit) { + final state = this.state; + + // Check if list is loaded + if (state is ItemListLoadedState) { + final items = state.items; + items.removeWhere((element) => element.id == event.itemObj.id); + + emit(ItemListLoadedState(items: items)); + } + } + + // ToggleItem event handler which emits a ItemToggled state + void _toggleItem(ToggleItemEvent event, Emitter emit) { + final state = this.state; + + // Check if list is loaded + if (state is ItemListLoadedState) { + // You have to create a new object because the items list needs to be new so Bloc knows it needs to re-render + // https://stackoverflow.com/questions/65379743/flutter-bloc-cant-update-my-list-of-boolean + final items = List.from(state.items); + final indexToChange = items.indexWhere((element) => element.id == event.itemObj.id); + + // If the element is found, we create a copy of the element with the `completed` field toggled. + if (indexToChange != -1) { + final itemToChange = items[indexToChange]; + final updatedItem = Item( + description: itemToChange.description, + completed: !itemToChange.completed, + ); + + items[indexToChange] = updatedItem; + } + + emit(ItemListLoadedState(items: [...items])); + } + } +} diff --git a/lib/blocs/item/item_event.dart b/lib/blocs/item/item_event.dart new file mode 100644 index 00000000..4bd11cd3 --- /dev/null +++ b/lib/blocs/item/item_event.dart @@ -0,0 +1,41 @@ +part of 'item_bloc.dart'; + +sealed class ItemEvent extends Equatable { + const ItemEvent(); +} + +/// Event to kick start the item list event +class ItemListStarted extends ItemEvent { + @override + List get props => []; +} + +/// AddItem event when an item is added +class AddItemEvent extends ItemEvent { + final Item itemObj; + + const AddItemEvent(this.itemObj); + + @override + List get props => [itemObj]; +} + +/// RemoveItem event when an item is removed +class RemoveItemEvent extends ItemEvent { + final Item itemObj; + + const RemoveItemEvent(this.itemObj); + + @override + List get props => [itemObj]; +} + +/// ToggleItem event when an item is toggled +class ToggleItemEvent extends ItemEvent { + final Item itemObj; + + const ToggleItemEvent(this.itemObj); + + @override + List get props => [itemObj]; +} diff --git a/lib/blocs/item/item_state.dart b/lib/blocs/item/item_state.dart new file mode 100644 index 00000000..c6226823 --- /dev/null +++ b/lib/blocs/item/item_state.dart @@ -0,0 +1,25 @@ +part of 'item_bloc.dart'; + +sealed class ItemState extends Equatable { + const ItemState(); +} + +/// Initial ItemBloc state +class ItemInitialState extends ItemState { + @override + List get props => []; +} + +/// ItemBloc state when the item list is loaded +class ItemListLoadedState extends ItemState { + final List items; + const ItemListLoadedState({this.items = const []}); + @override + List get props => [items]; +} + +/// ItemBloc state when an item errors when loading +class ItemListErrorState extends ItemState { + @override + List get props => []; +} diff --git a/lib/blocs/todo/todo_bloc.dart b/lib/blocs/todo/todo_bloc.dart deleted file mode 100644 index 05023e5a..00000000 --- a/lib/blocs/todo/todo_bloc.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../models/item.dart'; - -part 'todo_event.dart'; -part 'todo_state.dart'; - -/// This is the TodoBloc, -/// the bloc that manages the list of todos. -class TodoBloc extends Bloc { - // Bloc constructor and adding event handlers - TodoBloc() : super(TodoInitialState()) { - on(_onStart); - on(_addTodo); - on(_removeTodo); - on(_toggleTodo); - } - - void _onStart(TodoListStarted event, Emitter emit) { - // You could do stuff here like: - // 1. emit "loading state" - // 2. fetch todos from API - // 3. emit "success state" or "error state" - - emit(const TodoListLoadedState(items: [])); - } - - // AddTodo event handler which emits TodoAdded state - void _addTodo(AddTodoEvent event, Emitter emit) { - final state = this.state; - - // Check if list is loaded - if (state is TodoListLoadedState) { - emit(TodoListLoadedState(items: [...state.items, event.todoObj])); - } - } - - // RemoveTodo event handler which emits TodoDeleted state - void _removeTodo(RemoveTodoEvent event, Emitter emit) { - final state = this.state; - - // Check if list is loaded - if (state is TodoListLoadedState) { - final items = state.items; - items.removeWhere((element) => element.id == event.todoObj.id); - - emit(TodoListLoadedState(items: items)); - } - } - - // ToggleTodo event handler which emits a TodoToggled state - void _toggleTodo(ToggleTodoEvent event, Emitter emit) { - final state = this.state; - - // Check if list is loaded - if (state is TodoListLoadedState) { - // You have to create a new object because the items list needs to be new so Bloc knows it needs to re-render - // https://stackoverflow.com/questions/65379743/flutter-bloc-cant-update-my-list-of-boolean - final items = List.from(state.items); - final indexToChange = - items.indexWhere((element) => element.id == event.todoObj.id); - - // If the element is found, we create a copy of the element with the `completed` field toggled. - if (indexToChange != -1) { - final itemToChange = items[indexToChange]; - final updatedItem = Item( - description: itemToChange.description, - completed: !itemToChange.completed, - ); - - items[indexToChange] = updatedItem; - } - - emit(TodoListLoadedState(items: [...items])); - } - } -} diff --git a/lib/blocs/todo/todo_event.dart b/lib/blocs/todo/todo_event.dart deleted file mode 100644 index 6bf2cd8e..00000000 --- a/lib/blocs/todo/todo_event.dart +++ /dev/null @@ -1,41 +0,0 @@ -part of 'todo_bloc.dart'; - -sealed class TodoEvent extends Equatable { - const TodoEvent(); -} - -/// Event to kick start the todo list event -class TodoListStarted extends TodoEvent { - @override - List get props => []; -} - -/// AddTodo event when an item is added -class AddTodoEvent extends TodoEvent { - final Item todoObj; - - const AddTodoEvent(this.todoObj); - - @override - List get props => [todoObj]; -} - -/// RemoveTodo event when an item is removed -class RemoveTodoEvent extends TodoEvent { - final Item todoObj; - - const RemoveTodoEvent(this.todoObj); - - @override - List get props => [todoObj]; -} - -/// RemoveTodo event when an item is toggled -class ToggleTodoEvent extends TodoEvent { - final Item todoObj; - - const ToggleTodoEvent(this.todoObj); - - @override - List get props => [todoObj]; -} diff --git a/lib/blocs/todo/todo_state.dart b/lib/blocs/todo/todo_state.dart deleted file mode 100644 index 2f61caec..00000000 --- a/lib/blocs/todo/todo_state.dart +++ /dev/null @@ -1,25 +0,0 @@ -part of 'todo_bloc.dart'; - -sealed class TodoState extends Equatable { - const TodoState(); -} - -/// Initial TodoBloc state -class TodoInitialState extends TodoState { - @override - List get props => []; -} - -/// TodoBloc state when the todo item list is loaded -class TodoListLoadedState extends TodoState { - final List items; - const TodoListLoadedState({this.items = const []}); - @override - List get props => [items]; -} - -/// TodoBloc state when a todo item errors when loading -class TodoListErrorState extends TodoState { - @override - List get props => []; -} diff --git a/lib/main.dart b/lib/main.dart index 88b7aec6..e8911819 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,7 @@ void main() { runApp( MultiBlocProvider( providers: [ - BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), BlocProvider(create: (context) => AppCubit(isWeb: kIsWeb)), ], child: const MainApp(), @@ -27,7 +27,7 @@ void main() { /// The main class of the app. /// It will create the state manager with `BlocProvider` and make it available along the widget tree. /// -/// The `TodoListStarted` event is instantly spawned when the app starts. +/// The `ItemListStarted` event is instantly spawned when the app starts. /// This is because we've yet have the need to fetch information from third-party APIs before initializing the app. class MainApp extends StatelessWidget { const MainApp({super.key}); diff --git a/lib/models/item.dart b/lib/models/item.dart index 82ebfc7a..c2eacbf1 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -1,11 +1,11 @@ import 'package:flutter_quill/flutter_quill.dart'; import 'package:uuid/uuid.dart'; -// Uuid to generate Ids for the todos +// Uuid to generate UUIDs for the items const uuid = Uuid(); -/// Todo class. -/// Each `Todo` has an `id`, `description` and `completed` boolean field. +/// Item class. +/// Each [Item] has an [id], [description] and [completed] boolean field. /// Optionally, a `document` can be associated to the item. This [Document] is a rich text document. class Item { final String id = uuid.v4(); diff --git a/lib/presentation/views/home.dart b/lib/presentation/views/home.dart index cec2beff..1b2bc91b 100644 --- a/lib/presentation/views/home.dart +++ b/lib/presentation/views/home.dart @@ -3,12 +3,12 @@ import 'package:responsive_framework/responsive_framework.dart'; import '../../blocs/blocs.dart'; import '../widgets/widgets.dart'; -import 'new_todo.dart'; +import 'new_item.dart'; const textfieldKey = Key('textfieldKey'); /// App's home page. -/// The person will be able to create a new todo item +/// The person will be able to create a new item item /// and view a list of previously created ones. class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -23,10 +23,10 @@ class HomePage extends StatelessWidget { // Body of the page. // It is responsive and change style according to the device. - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { // If the list is loaded - if (state is TodoListLoadedState) { + if (state is ItemListLoadedState) { final items = state.items; return SafeArea( @@ -41,8 +41,7 @@ class HomePage extends StatelessWidget { controller: TextEditingController(), keyboardType: TextInputType.none, onTap: () { - Navigator.of(context) - .push(navigateToNewTodoItemPage()); + Navigator.of(context).push(navigateToCreateNewItemPage()); }, maxLines: 2, style: const TextStyle(fontSize: 20), @@ -63,8 +62,7 @@ class HomePage extends StatelessWidget { controller: TextEditingController(), keyboardType: TextInputType.none, onTap: () { - Navigator.of(context) - .push(navigateToNewTodoItemPage()); + Navigator.of(context).push(navigateToCreateNewItemPage()); }, maxLines: 2, style: const TextStyle(fontSize: 30), @@ -103,7 +101,7 @@ class HomePage extends StatelessWidget { ); } - // If the state of the TodoItemList is not loaded, we show error.ˆ + // If the state of the ItemList is not loaded, we show error.ˆ else { return const Center(child: Text('Error loading items list.')); } diff --git a/lib/presentation/views/new_todo.dart b/lib/presentation/views/new_item.dart similarity index 85% rename from lib/presentation/views/new_todo.dart rename to lib/presentation/views/new_item.dart index abfcb704..8e6e71ae 100644 --- a/lib/presentation/views/new_todo.dart +++ b/lib/presentation/views/new_item.dart @@ -9,23 +9,23 @@ import '../../models/models.dart'; const saveButtonKey = Key('saveButtonKey'); /// Transition handler that navigates the route to the `NewTodo` item page. -Route navigateToNewTodoItemPage() { +Route navigateToCreateNewItemPage() { return PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => const NewTodoPage(), + pageBuilder: (context, animation, secondaryAnimation) => const NewItemPage(), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ); } -/// Page that shows a textfield expanded to create a new todo item. -class NewTodoPage extends StatefulWidget { - const NewTodoPage({super.key}); +/// Page that shows a textfield expanded to create a new item. +class NewItemPage extends StatefulWidget { + const NewItemPage({super.key}); @override - State createState() => _NewTodoPageState(); + State createState() => _NewItemPageState(); } -class _NewTodoPageState extends State { +class _NewItemPageState extends State { final _controller = QuillController( document: Document(), selection: const TextSelection.collapsed(offset: 0), @@ -64,13 +64,14 @@ class _NewTodoPageState extends State { children: [ // Textfield that is expanded and borderless Expanded( - child: DeltaTodoEditor( - isWeb: isWeb, - editorController: _controller, - ),), + child: DeltaTodoEditor( + isWeb: isWeb, + editorController: _controller, + ), + ), // Save button. - // When submitted, it adds a new todo item, clears the controller and navigates back + // When submitted, it adds a new item, clears the controller and navigates back Align( alignment: Alignment.bottomRight, child: ElevatedButton( @@ -94,7 +95,7 @@ class _NewTodoPageState extends State { if (text.isNotEmpty) { // Create new item and create AddTodo event final newTodoItem = Item(description: text, document: document); - BlocProvider.of(context).add(AddTodoEvent(newTodoItem)); + BlocProvider.of(context).add(AddItemEvent(newTodoItem)); // Clear textfield _controller.clear(); diff --git a/lib/presentation/views/views.dart b/lib/presentation/views/views.dart index b5f9c407..573701d4 100644 --- a/lib/presentation/views/views.dart +++ b/lib/presentation/views/views.dart @@ -1,2 +1,2 @@ export 'home.dart'; -export 'new_todo.dart'; +export 'new_item.dart'; diff --git a/lib/presentation/widgets/editor/todo_editor.dart b/lib/presentation/widgets/editor/item_editor.dart similarity index 100% rename from lib/presentation/widgets/editor/todo_editor.dart rename to lib/presentation/widgets/editor/item_editor.dart diff --git a/lib/presentation/widgets/items.dart b/lib/presentation/widgets/items.dart index b354f6ea..fbe2b17c 100644 --- a/lib/presentation/widgets/items.dart +++ b/lib/presentation/widgets/items.dart @@ -32,8 +32,7 @@ class _ItemCardState extends State { super.initState(); WidgetsFlutterBinding.ensureInitialized(); - _stopwatch = - TimerStopwatch(initialOffset: widget.item.getCumulativeDuration()); + _stopwatch = TimerStopwatch(initialOffset: widget.item.getCumulativeDuration()); // Timer to rerender the page so the text shows the seconds passing by _timer = Timer.periodic(const Duration(milliseconds: 200), (timer) { @@ -63,7 +62,7 @@ class _ItemCardState extends State { // Start and stop timer button handler void _handleButtonClick() { - // If timer is ongoing, we stop the stopwatch and the timer in the todo item. + // If timer is ongoing, we stop the stopwatch and the timer in the item. if (_stopwatch.isRunning) { widget.item.stopTimer(); _stopwatch.stop(); @@ -72,7 +71,7 @@ class _ItemCardState extends State { setState(() {}); } - // If we are to start timer, start the timer in todo item and stopwatch. + // If we are to start timer, start the timer in item and stopwatch. else { widget.item.startTimer(); _stopwatch.start(); @@ -113,12 +112,12 @@ class _ItemCardState extends State { onTap: () { // If the stopwatch is not running, we mark toggle it if (!_stopwatch.isRunning) { - context.read().add(ToggleTodoEvent(widget.item)); + context.read().add(ToggleItemEvent(widget.item)); } // If the stopwatch is running, we toggle the item but also stop the timer else { - context.read().add(ToggleTodoEvent(widget.item)); + context.read().add(ToggleItemEvent(widget.item)); widget.item.stopTimer(); _stopwatch.stop(); @@ -148,7 +147,7 @@ class _ItemCardState extends State { title: Row( children: [ - // Todo item description + // Item description Expanded( child: Container( margin: const EdgeInsets.only(right: 16.0), @@ -159,15 +158,9 @@ class _ItemCardState extends State { widget.item.description, style: TextStyle( fontSize: 20, - decoration: widget.item.completed - ? TextDecoration.lineThrough - : TextDecoration.none, - fontStyle: widget.item.completed - ? FontStyle.italic - : FontStyle.normal, - color: widget.item.completed - ? const Color.fromARGB(255, 126, 121, 121) - : Colors.black, + decoration: widget.item.completed ? TextDecoration.lineThrough : TextDecoration.none, + fontStyle: widget.item.completed ? FontStyle.italic : FontStyle.normal, + color: widget.item.completed ? const Color.fromARGB(255, 126, 121, 121) : Colors.black, ), ); } @@ -178,15 +171,9 @@ class _ItemCardState extends State { widget.item.description, style: TextStyle( fontSize: 25, - decoration: widget.item.completed - ? TextDecoration.lineThrough - : TextDecoration.none, - fontStyle: widget.item.completed - ? FontStyle.italic - : FontStyle.normal, - color: widget.item.completed - ? const Color.fromARGB(255, 126, 121, 121) - : Colors.black, + decoration: widget.item.completed ? TextDecoration.lineThrough : TextDecoration.none, + fontStyle: widget.item.completed ? FontStyle.italic : FontStyle.normal, + color: widget.item.completed ? const Color.fromARGB(255, 126, 121, 121) : Colors.black, ), ); } diff --git a/lib/presentation/widgets/widgets.dart b/lib/presentation/widgets/widgets.dart index ddca2fb3..59419b34 100644 --- a/lib/presentation/widgets/widgets.dart +++ b/lib/presentation/widgets/widgets.dart @@ -1,3 +1,3 @@ export 'items.dart'; export 'navbar.dart'; -export 'editor/todo_editor.dart'; +export 'editor/item_editor.dart'; diff --git a/test/bloc/log_bloc_observer_test.dart b/test/bloc/log_bloc_observer_test.dart index 442e709e..1d9c4143 100644 --- a/test/bloc/log_bloc_observer_test.dart +++ b/test/bloc/log_bloc_observer_test.dart @@ -1,20 +1,19 @@ -import 'package:dwyl_app/blocs/todo/todo_bloc.dart'; +import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/logging/logging.dart'; import 'package:dwyl_app/models/item.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; // This is similar to Bloc's testing. // See https://github.com/felangel/bloc/blob/master/packages/bloc/test/bloc_observer_test.dart. void main() { - final bloc = TodoBloc(); + final bloc = ItemBloc(); final error = Exception(); const stackTrace = StackTrace.empty; - const event = AddTodoEvent; + const event = AddItemEvent; final change = Change(currentState: const [], nextState: [Item(description: 'description')]); final transition = Transition( currentState: const [], - event: AddTodoEvent, + event: AddItemEvent, nextState: [Item(description: 'description')], ); group('BlocObserver', () { diff --git a/test/bloc/todo/todo_bloc_test.dart b/test/bloc/todo/todo_bloc_test.dart index a3aab1fc..7731e091 100644 --- a/test/bloc/todo/todo_bloc_test.dart +++ b/test/bloc/todo/todo_bloc_test.dart @@ -4,55 +4,55 @@ import 'package:dwyl_app/models/item.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('TodoBloc', () { + group('ItemBloc', () { // List of items to mock - final newItem = Item(description: 'todo description'); + final newItem = Item(description: 'Item description'); blocTest( 'emits [] when nothing is added', - build: () => TodoBloc(), + build: () => ItemBloc(), expect: () => [], ); blocTest( - 'emits [TodoListLoadedState] when AddTodoEvent is created', - build: () => TodoBloc()..add(TodoListStarted()), + 'emits [ItemListLoadedState] when AddItemEvent is created', + build: () => ItemBloc()..add(ItemListStarted()), act: (bloc) { - bloc.add(AddTodoEvent(newItem)); + bloc.add(AddItemEvent(newItem)); }, - expect: () => [ - const TodoListLoadedState(items: []), // when the todo bloc was loaded - TodoListLoadedState( + expect: () => [ + const ItemListLoadedState(items: []), // when the item bloc was loaded + ItemListLoadedState( items: [newItem], - ), // when the todo bloc was added an event + ), // when the item bloc was added an event ], ); blocTest( - 'emits [TodoListLoadedState] when RemoveTodoEvent is created', - build: () => TodoBloc()..add(TodoListStarted()), + 'emits [ItemListLoadedState] when RemoveItemEvent is created', + build: () => ItemBloc()..add(ItemListStarted()), act: (bloc) { - final newItem = Item(description: 'todo description'); + final newItem = Item(description: 'Item description'); bloc - ..add(AddTodoEvent(newItem)) - ..add(RemoveTodoEvent(newItem)); // add and remove + ..add(AddItemEvent(newItem)) + ..add(RemoveItemEvent(newItem)); // add and remove }, - expect: () => [const TodoListLoadedState(items: []), const TodoListLoadedState(items: [])], + expect: () => [const ItemListLoadedState(items: []), const ItemListLoadedState(items: [])], ); blocTest( - 'emits [TodoListLoadedState] when ToggleTodoEvent is created', - build: () => TodoBloc()..add(TodoListStarted()), + 'emits [ItemListLoadedState] when ToggleItemEvent is created', + build: () => ItemBloc()..add(ItemListStarted()), act: (bloc) { - final newItem = Item(description: 'todo description'); + final newItem = Item(description: 'Item description'); bloc - ..add(AddTodoEvent(newItem)) - ..add(ToggleTodoEvent(newItem)); + ..add(AddItemEvent(newItem)) + ..add(ToggleItemEvent(newItem)); }, expect: () => [ - isA(), - isA().having((obj) => obj.items.first.completed, 'completed', false), - isA().having((obj) => obj.items.first.completed, 'completed', true), + isA(), + isA().having((obj) => obj.items.first.completed, 'completed', false), + isA().having((obj) => obj.items.first.completed, 'completed', true), ], ); }); diff --git a/test/bloc/todo/todo_event_test.dart b/test/bloc/todo/todo_event_test.dart index ae701431..26518ea8 100644 --- a/test/bloc/todo/todo_event_test.dart +++ b/test/bloc/todo/todo_event_test.dart @@ -3,31 +3,31 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:dwyl_app/models/item.dart'; void main() { - group('TodoEvent', () { - group('TodoListStarted', () { + group('ItemEvent', () { + group('ItemListStarted', () { test('supports value comparison', () { - expect(TodoListStarted(), TodoListStarted()); + expect(ItemListStarted(), ItemListStarted()); }); }); - group('AddTodoEvent', () { + group('AddItemEvent', () { final item = Item(description: 'description'); test('supports value comparison', () { - expect(AddTodoEvent(item), AddTodoEvent(item)); + expect(AddItemEvent(item), AddItemEvent(item)); }); }); - group('RemoveTodoEvent', () { + group('RemoveItemEvent', () { final item = Item(description: 'description'); test('supports value comparison', () { - expect(RemoveTodoEvent(item), RemoveTodoEvent(item)); + expect(RemoveItemEvent(item), RemoveItemEvent(item)); }); }); - group('ToggleTodoEvent', () { + group('ToggleItemEvent', () { final item = Item(description: 'description'); test('supports value comparison', () { - expect(ToggleTodoEvent(item), ToggleTodoEvent(item)); + expect(ToggleItemEvent(item), ToggleItemEvent(item)); }); }); }); diff --git a/test/bloc/todo/todo_state_test.dart b/test/bloc/todo/todo_state_test.dart index 4850b7d0..56db2438 100644 --- a/test/bloc/todo/todo_state_test.dart +++ b/test/bloc/todo/todo_state_test.dart @@ -2,22 +2,22 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:dwyl_app/blocs/blocs.dart'; void main() { - group('TodoState', () { - group('TodoInitialState', () { + group('ItemState', () { + group('ItemInitialState', () { test('supports value comparison', () { - expect(TodoInitialState(), TodoInitialState()); + expect(ItemInitialState(), ItemInitialState()); }); }); - group('TodoListLoadedState', () { + group('ItemListLoadedState', () { test('supports value comparison', () { - expect(const TodoListLoadedState(), const TodoListLoadedState()); + expect(const ItemListLoadedState(), const ItemListLoadedState()); }); }); - group('TodoListErrorState', () { + group('ItemListErrorState', () { test('supports value comparison', () { - expect(TodoListErrorState(), TodoListErrorState()); + expect(ItemListErrorState(), ItemListErrorState()); }); }); }); diff --git a/test/integration_test.dart b/test/integration_test.dart index ddf607db..f088858c 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -13,7 +13,7 @@ void main() { Widget initializeMainApp({required bool isWeb}) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), ], child: const MainApp(), @@ -30,33 +30,33 @@ void main() { await tester.pumpWidget(app); await tester.pump(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); }); }); - group('Adding a new todo item', () { + group('Adding a new item', () { testWidgets('shows a card', (WidgetTester tester) async { final app = initializeMainApp(isWeb: false); await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); final editor = find.byType(QuillEditor); - // Type text into todo editor + // Type text into item editor await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); + await tester.quillEnterText(editor, 'Make lunch\n'); await tester.pumpAndSettle(); - // Tap "Save" button to add new todo item + // Tap "Save" button to add new item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -77,11 +77,11 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -91,12 +91,12 @@ void main() { // await tester.tap(find.byKey(navBarInNewTodoPageKey)); // await tester.pumpAndSettle(); - // Type text into todo input + // Type text into item input await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); + await tester.quillEnterText(editor, 'Make lunch\n'); await tester.pumpAndSettle(); - // Tap "Save" button to add new todo item + // Tap "Save" button to add new item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -120,22 +120,22 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); final editor = find.byType(QuillEditor); - // Type text into todo input + // Type text into item input await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); + await tester.quillEnterText(editor, 'Make lunch\n'); await tester.pumpAndSettle(); - // Tap "Save" button to add new todo item + // Tap "Save" button to add new item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -159,22 +159,22 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); final editor = find.byType(QuillEditor); - // Type text into todo input + // Type text into item input await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); + await tester.quillEnterText(editor, 'Make lunch\n'); await tester.pumpAndSettle(); - // Tap "Save" button to add new todo item + // Tap "Save" button to add new item await tester.tap(find.byKey(saveButtonKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -193,19 +193,19 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); - // Type text into todo input and tap "Save" button to add new todo item + // Type text into item input and tap "Save" button to add new item final editor = find.byType(QuillEditor); await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); + await tester.quillEnterText(editor, 'Make lunch\n'); await tester.pumpAndSettle(); await tester.tap(find.byKey(saveButtonKey)); @@ -237,19 +237,19 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); - // Type text into todo input and tap "Save" button to add new todo item + // Type text into item input and tap "Save" button to add new item final editor = find.byType(QuillEditor); await tester.tap(editor); - await tester.quillEnterText(editor, 'new todo\n'); + await tester.quillEnterText(editor, 'Make lunch\n'); await tester.pumpAndSettle(); await tester.tap(find.byKey(saveButtonKey)); @@ -324,11 +324,11 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -372,11 +372,11 @@ void main() { await tester.pumpWidget(app); await tester.pumpAndSettle(); - // Find the text input and string stating 0 todos created + // Find the text input and string stating 0 items created expect(find.byKey(textfieldKey), findsOneWidget); expect(find.byKey(itemCardWidgetKey), findsNothing); - // Tap textfield to open new page to create todo item + // Tap textfield to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); @@ -389,7 +389,7 @@ void main() { // User went back to the home page expect(find.byKey(textfieldKey), findsOneWidget); - // Tap textfield again to open new page to create todo item + // Tap textfield again to open new page to create item await tester.tap(find.byKey(textfieldKey)); await tester.pumpAndSettle(const Duration(seconds: 2)); diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart index 8ab6c780..92ffbec4 100644 --- a/test/widget/emoji_widget_test.dart +++ b/test/widget/emoji_widget_test.dart @@ -1,4 +1,3 @@ - import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/main.dart'; import 'package:dwyl_app/presentation/views/views.dart'; @@ -8,15 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; - - void main() { - /// Bootstraps a sample main application, whether it [isWeb] or not. Widget initializeMainApp({required bool isWeb}) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => TodoBloc()..add(TodoListStarted())), + BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), ], child: const MainApp(), @@ -27,6 +23,4 @@ void main() { setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); - - } diff --git a/web/manifest.json b/web/manifest.json index 477f2bee..a68ce5c9 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "Dwyl's Todo App", - "short_name": "todo app", + "name": "Dwyl's App", + "short_name": "dwyl app", "start_url": ".", "display": "standalone", "background_color": "#0175C2", From db86d11787a9d819112b7c21ef05162df42d33b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 11:31:27 +0100 Subject: [PATCH 25/43] chore: Adding new_item_widget_test. #275 --- lib/presentation/views/new_item.dart | 2 + test/widget/new_item_widget_test.dart | 61 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/widget/new_item_widget_test.dart diff --git a/lib/presentation/views/new_item.dart b/lib/presentation/views/new_item.dart index 8e6e71ae..10f0ab5f 100644 --- a/lib/presentation/views/new_item.dart +++ b/lib/presentation/views/new_item.dart @@ -7,6 +7,7 @@ import '../../blocs/blocs.dart'; import '../../models/models.dart'; const saveButtonKey = Key('saveButtonKey'); +const newItemPageNavbarKey = Key('newItemPageNavbarKey'); /// Transition handler that navigates the route to the `NewTodo` item page. Route navigateToCreateNewItemPage() { @@ -55,6 +56,7 @@ class _NewItemPageState extends State { return MaterialApp( home: Scaffold( appBar: NavBar( + key: newItemPageNavbarKey, givenContext: context, showGoBackButton: true, onTap: () => _onTapNavbar(context), diff --git a/test/widget/new_item_widget_test.dart b/test/widget/new_item_widget_test.dart new file mode 100644 index 00000000..5f69856d --- /dev/null +++ b/test/widget/new_item_widget_test.dart @@ -0,0 +1,61 @@ +import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/presentation/views/views.dart'; +import 'package:dwyl_app/presentation/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:dwyl_app/main.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + +void main() { + /// Bootstraps a `NewItemPage`. + Widget initializeNewItemPage({required bool isWeb}) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), + ], + child: MaterialApp( + home: const NewItemPage(), + builder: (context, child) => ResponsiveBreakpoints.builder( + child: child!, + breakpoints: [ + const Breakpoint(start: 0, end: 425, name: MOBILE), + const Breakpoint(start: 426, end: 768, name: TABLET), + const Breakpoint(start: 769, end: 1024, name: DESKTOP), + const Breakpoint(start: 1025, end: 1440, name: 'LARGE_DESKTOP'), + const Breakpoint(start: 1441, end: double.infinity, name: '4K'), + ], + ), + ), + ); + } + + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('New item page', () { + testWidgets('should lose focus when clicking on navbar.', (WidgetTester tester) async { + final app = initializeNewItemPage(isWeb: false); + + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Check if the editor is visible and tap it + final editor = find.byType(QuillEditor); + expect(editor.hitTestable(), findsOneWidget); + + await tester.tap(editor); + await tester.pumpAndSettle(); + + // Tap on navbar + final navbar = find.byKey(newItemPageNavbarKey); + await tester.tap(navbar); + await tester.pumpAndSettle(); + + expect(editor.hitTestable(), findsOneWidget); + }); + }); +} From e1f69e861f1e1ded7705a53fdcde06d71e10f635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 12:43:16 +0100 Subject: [PATCH 26/43] chore: Add emoji picker test. #275 --- .../widgets/editor/emoji_picker.dart | 8 ++-- test/integration_test.dart | 4 -- test/widget/emoji_widget_test.dart | 40 +++++++++++++++++-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/lib/presentation/widgets/editor/emoji_picker.dart b/lib/presentation/widgets/editor/emoji_picker.dart index 3717a7cb..4d2f6b0b 100644 --- a/lib/presentation/widgets/editor/emoji_picker.dart +++ b/lib/presentation/widgets/editor/emoji_picker.dart @@ -24,18 +24,18 @@ class _OffstageEmojiPickerState extends State { /// Returns the emoji picker configuration according to screen size. Config _buildEmojiPickerConfig(BuildContext context) { if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) { - return const Config(emojiSizeMax: 32.0, columns: 7); + return const Config(emojiSizeMax: 32.0, columns: 7, recentTabBehavior: RecentTabBehavior.NONE); } if (ResponsiveBreakpoints.of(context).equals(TABLET)) { - return const Config(emojiSizeMax: 24.0, columns: 10); + return const Config(emojiSizeMax: 24.0, columns: 10, recentTabBehavior: RecentTabBehavior.NONE); } if (ResponsiveBreakpoints.of(context).equals(DESKTOP)) { - return const Config(emojiSizeMax: 16.0, columns: 15); + return const Config(emojiSizeMax: 16.0, columns: 15, recentTabBehavior: RecentTabBehavior.NONE); } - return const Config(emojiSizeMax: 16.0, columns: 30); + return const Config(emojiSizeMax: 16.0, columns: 30, recentTabBehavior: RecentTabBehavior.NONE); } @override diff --git a/test/integration_test.dart b/test/integration_test.dart index f088858c..d5f4c044 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -87,10 +87,6 @@ void main() { final editor = find.byType(QuillEditor); - // Lose focus - // await tester.tap(find.byKey(navBarInNewTodoPageKey)); - // await tester.pumpAndSettle(); - // Type text into item input await tester.tap(editor); await tester.quillEnterText(editor, 'Make lunch\n'); diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart index 92ffbec4..86493f80 100644 --- a/test/widget/emoji_widget_test.dart +++ b/test/widget/emoji_widget_test.dart @@ -3,19 +3,35 @@ import 'package:dwyl_app/main.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:responsive_framework/responsive_framework.dart'; void main() { - /// Bootstraps a sample main application, whether it [isWeb] or not. - Widget initializeMainApp({required bool isWeb}) { + /// Bootstraps a `OffstageEmojiPicker`. + Widget initializeOffstageEmojiPicker({required bool isWeb, required bool isOffstage}) { return MultiBlocProvider( providers: [ BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), BlocProvider(create: (context) => AppCubit(isWeb: isWeb)), ], - child: const MainApp(), + child: MaterialApp( + home: OffstageEmojiPicker( + offstageEmojiPicker: isOffstage, + ), + builder: (context, child) => ResponsiveBreakpoints.builder( + child: child!, + breakpoints: [ + const Breakpoint(start: 0, end: 425, name: MOBILE), + const Breakpoint(start: 426, end: 768, name: TABLET), + const Breakpoint(start: 769, end: 1024, name: DESKTOP), + const Breakpoint(start: 1025, end: 1440, name: 'LARGE_DESKTOP'), + const Breakpoint(start: 1441, end: double.infinity, name: '4K'), + ], + ), + ), ); } @@ -23,4 +39,22 @@ void main() { setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); + + group('Emoji Picker', () { + testWidgets('should be shown and tap on emoji.', (WidgetTester tester) async { + // Initialize widget that should show picker + final app = initializeOffstageEmojiPicker(isWeb: false, isOffstage: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + expect(find.byType(EmojiPicker), findsOneWidget); + + // Tap on emoji + final emoji = find.text('😍'); + await tester.tap(emoji); + await tester.pumpAndSettle(); + + expect(find.byType(EmojiPicker), findsOneWidget); + }); + }); } From b48803f839c20af1b7144c3cf474ef132ab8b237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 12:45:11 +0100 Subject: [PATCH 27/43] fix: Format. --- test/widget/emoji_widget_test.dart | 4 ---- test/widget/new_item_widget_test.dart | 3 --- 2 files changed, 7 deletions(-) diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart index 86493f80..ca4d32f2 100644 --- a/test/widget/emoji_widget_test.dart +++ b/test/widget/emoji_widget_test.dart @@ -1,11 +1,7 @@ import 'package:dwyl_app/blocs/blocs.dart'; -import 'package:dwyl_app/main.dart'; -import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; -import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:responsive_framework/responsive_framework.dart'; diff --git a/test/widget/new_item_widget_test.dart b/test/widget/new_item_widget_test.dart index 5f69856d..3df68fe7 100644 --- a/test/widget/new_item_widget_test.dart +++ b/test/widget/new_item_widget_test.dart @@ -1,11 +1,8 @@ import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/presentation/views/views.dart'; -import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; -import 'package:flutter_quill/flutter_quill_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:dwyl_app/main.dart'; import 'package:responsive_framework/responsive_framework.dart'; void main() { From 53f263acfb86e585ba786c156ec8b1a67c68f5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 16:37:24 +0100 Subject: [PATCH 28/43] chore: Add emoji tests. #275 --- test/integration_test.dart | 77 +++++++++++++++++++++++++++++- test/widget/emoji_widget_test.dart | 4 ++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index d5f4c044..f116fb89 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1,10 +1,13 @@ +import 'package:cross_file/cross_file.dart'; import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:dwyl_app/main.dart'; @@ -307,7 +310,7 @@ void main() { }); group('Emoji picker', () { - testWidgets('should shown when clicking in the emoji button.', (WidgetTester tester) async { + testWidgets('should be shown when clicking in the emoji button.', (WidgetTester tester) async { // Set size because it's needed to correctly tap on emoji picker // and ensure binding is initialized to setup camera size TestWidgetsFlutterBinding.ensureInitialized(); @@ -360,6 +363,78 @@ void main() { expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); }); + + testWidgets('should lose focus of keyboard when double tapped', (WidgetTester tester) async { + // Set size because it's needed to correctly tap on emoji picker + // and ensure binding is initialized to setup camera size + TestWidgetsFlutterBinding.ensureInitialized(); + tester.view.physicalSize = const Size(380, 800); + tester.view.devicePixelRatio = 1.0; + await tester.binding.setSurfaceSize(const Size(380, 800)); + + // Initialize app + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Find the text input and string stating 0 items created + expect(find.byKey(textfieldKey), findsOneWidget); + expect(find.byKey(itemCardWidgetKey), findsNothing); + + // Tap textfield to open new page to create item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Expect to find the normal page setup and emoji picker not being shown + final editor = find.byType(QuillEditor); + expect(editor.hitTestable(), findsOneWidget); + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsNothing); + + // Tap on editor to gain focus + await tester.tap(editor); + await tester.pumpAndSettle(); + + // Click on emoji button should show the emoji picker + var emojiIcon = find.byIcon(Icons.emoji_emotions); + await tester.tap(emojiIcon); + await tester.pumpAndSettle(); + + expect(find.byKey(emojiPickerWidgetKey).hitTestable(), findsOneWidget); + }); + }); + + group('Image picker', () { + testWidgets('should show and select image', (WidgetTester tester) async { + // Mock image + // mockImagePicker(tester); + + // Build our app and trigger a frame. + final app = initializeMainApp(isWeb: false); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Tap textfield to open new page to create item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Should show editor and toolbar + final editor = find.byType(QuillEditor); + final toolbar = find.byType(QuillToolbar); + expect(editor.hitTestable(), findsOneWidget); + expect(toolbar.hitTestable(), findsOneWidget); + + // Drag toolbar to the right + await tester.drag(toolbar, const Offset(-500, 0)); + await tester.pump(const Duration(minutes: 1)); + + // Press image button + final imageButton = find.byType(ImageButton); + await tester.tap(imageButton); + await tester.pumpAndSettle(); + + // TODO can't choose image. + // Check https://github.com/singerdmx/flutter-quill/issues/1389. + }); }); group('Navigation', () { diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart index ca4d32f2..86493f80 100644 --- a/test/widget/emoji_widget_test.dart +++ b/test/widget/emoji_widget_test.dart @@ -1,7 +1,11 @@ import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/main.dart'; +import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; +import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:responsive_framework/responsive_framework.dart'; From 86fa6ff74b623fca2511f1ae78df5a3144c428b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 16:42:04 +0100 Subject: [PATCH 29/43] chore: Adding dev dependencies. #275 --- pubspec.lock | 49 ++++++++++++++++++++++++++++++++++++++++++++----- pubspec.yaml | 4 ++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 755fad93..d2cdec65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -122,7 +122,7 @@ packages: source: hosted version: "1.6.3" cross_file: - dependency: transitive + dependency: "direct dev" description: name: cross_file sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb @@ -270,6 +270,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_inappwebview: dependency: transitive description: @@ -397,6 +402,11 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" gallery_saver: dependency: transitive description: @@ -525,6 +535,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: transitive description: @@ -753,10 +768,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -773,6 +788,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" provider: dependency: transitive description: @@ -962,6 +985,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -1174,10 +1205,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ada49637c27973c183dad90beb6bd781eea4c9f5f955d35da172de0af7bd3440 + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f url: "https://pub.dev" source: hosted - version: "11.8.0" + version: "11.7.1" watcher: dependency: transitive description: @@ -1202,6 +1233,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + url: "https://pub.dev" + source: hosted + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 89e14391..5e042603 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,10 +39,14 @@ dependencies: colorize_lumberdash: ^3.0.0 dev_dependencies: + integration_test: + sdk: flutter flutter_test: sdk: flutter flutter_lints: ^2.0.2 + cross_file: ^0.3.3+5 + flutter: uses-material-design: true From e2f2c59952b9eec8e26a7c77379408d8cdb366ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 3 Oct 2023 18:18:34 +0100 Subject: [PATCH 30/43] chore: Adding tests for image callbacks. #275 --- .../widgets/editor/image_callbacks.dart | 6 +- .../widgets/editor/item_editor.dart | 3 +- pubspec.lock | 154 ++- pubspec.yaml | 4 + test/integration_test.dart | 4 +- test/sample.jpeg | Bin 0 -> 18625 bytes test/unit/image_callbacks_test.dart | 89 ++ test/unit/image_callbacks_test.mocks.dart | 997 ++++++++++++++++++ test/widget/emoji_widget_test.dart | 4 - 9 files changed, 1249 insertions(+), 12 deletions(-) create mode 100644 test/sample.jpeg create mode 100644 test/unit/image_callbacks_test.dart create mode 100644 test/unit/image_callbacks_test.mocks.dart diff --git a/lib/presentation/widgets/editor/image_callbacks.dart b/lib/presentation/widgets/editor/image_callbacks.dart index bee1e43c..0a0c476d 100644 --- a/lib/presentation/widgets/editor/image_callbacks.dart +++ b/lib/presentation/widgets/editor/image_callbacks.dart @@ -24,9 +24,9 @@ Future onImagePickCallback(File file) async { /// Upon picking an image, it is uploaded and the URL of where the image is hosted is returned. /// /// Returns `null` if no image was picked or the image was not correctly uploaded. -Future webImagePickImpl(OnImagePickCallback onImagePickCallback) async { +Future webImagePickImpl(http.Client client, ImageFilePicker filePicker, OnImagePickCallback onImagePickCallback) async { // Lets the user pick one file; files with any file extension can be selected - final result = await ImageFilePicker().pickImage(); + final result = await filePicker.pickImage(); // The result will be null, if the user aborted the dialog if (result == null || result.files.isEmpty) { @@ -53,7 +53,7 @@ Future webImagePickImpl(OnImagePickCallback onImagePickCallback) async request.files.add(httpImage); // Check the response and handle accordingly - return http.Client().send(request).then((response) async { + return client.send(request).then((response) async { if (response.statusCode != 200) { return null; } diff --git a/lib/presentation/widgets/editor/item_editor.dart b/lib/presentation/widgets/editor/item_editor.dart index f84c3ab8..5b034dfd 100644 --- a/lib/presentation/widgets/editor/item_editor.dart +++ b/lib/presentation/widgets/editor/item_editor.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:http/http.dart' as http; import 'image_callbacks.dart'; import 'web_embeds/web_embeds.dart'; @@ -179,7 +180,7 @@ class DeltaTodoEditorState extends State { onImagePickCallback: onImagePickCallback, // `webImagePickImpl` is called after image is picked on the web - webImagePickImpl: webImagePickImpl, + webImagePickImpl: (onImagePickCallback) => webImagePickImpl(http.Client(), ImageFilePicker(), onImagePickCallback), // defining the selector (we only want to open the gallery whenever the person wants to upload an image) mediaPickSettingSelector: (context) { diff --git a/pubspec.lock b/pubspec.lock index d2cdec65..66bb96ab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + url: "https://pub.dev" + source: hosted + version: "7.2.11" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + url: "https://pub.dev" + source: hosted + version: "8.6.3" characters: dependency: transitive description: @@ -81,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -89,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + url: "https://pub.dev" + source: hosted + version: "4.7.0" collection: dependency: transitive description: @@ -145,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" device_info_plus: dependency: transitive description: @@ -249,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -423,6 +519,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" holding_gesture: dependency: transitive description: @@ -564,6 +668,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" lints: dependency: transitive description: @@ -636,6 +748,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" + source: hosted + version: "5.4.2" mocktail: dependency: transitive description: @@ -725,7 +845,7 @@ packages: source: hosted version: "2.2.1" path_provider_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: path_provider_platform_interface sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" @@ -812,6 +932,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" quiver: dependency: transitive description: @@ -921,6 +1049,14 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" source_map_stack_trace: dependency: transitive description: @@ -969,6 +1105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1025,6 +1169,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.3" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5e042603..bdc7701c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,11 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.2 + mockito: ^5.4.2 + build_runner: ^2.4.5 + cross_file: ^0.3.3+5 + path_provider_platform_interface: ^2.1.1 flutter: uses-material-design: true diff --git a/test/integration_test.dart b/test/integration_test.dart index f116fb89..d306dabc 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1,10 +1,8 @@ -import 'package:cross_file/cross_file.dart'; import 'package:dwyl_app/blocs/blocs.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill_test.dart'; import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; @@ -395,7 +393,7 @@ void main() { await tester.pumpAndSettle(); // Click on emoji button should show the emoji picker - var emojiIcon = find.byIcon(Icons.emoji_emotions); + final emojiIcon = find.byIcon(Icons.emoji_emotions); await tester.tap(emojiIcon); await tester.pumpAndSettle(); diff --git a/test/sample.jpeg b/test/sample.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..24c6d9e75bfe24b3a080874e7f566b92a9616366 GIT binary patch literal 18625 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 I>jg9a*|vz~1poj5 literal 0 HcmV?d00001 diff --git a/test/unit/image_callbacks_test.dart b/test/unit/image_callbacks_test.dart new file mode 100644 index 00000000..bbe71f92 --- /dev/null +++ b/test/unit/image_callbacks_test.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dwyl_app/presentation/widgets/editor/image_callbacks.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/mockito.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +import 'image_callbacks_test.mocks.dart'; + +/// Class that is used to override the `getApplicationDocumentsDirectory()` function. +class FakePathProviderPlatform extends PathProviderPlatform implements Fake { + @override + Future getApplicationDocumentsPath() async { + // Make sure is the same folder used in the tests. + return 'test'; + } +} + +/// File mock (overrides `dart.io`) +/// Visit https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html for more information +/// and https://stackoverflow.com/questions/64031671/flutter-readasbytes-readasstring-in-widget-tests for context on why `readAsBytes` is skipped on tests. +/// This is used to mock the `File` class (useful for `readAsBytes`) +class FileMock extends MockFile { + @override + Future readAsBytes() { + final bytes = Uint8List(0); + return Future.value(bytes); + } + + @override + String get path => 'some_path.png'; +} + +@GenerateMocks([http.Client, ImageFilePicker, File]) +void main() { + /// Check for context: https://stackoverflow.com/questions/60671728/unable-to-load-assets-in-flutter-tests + setUp(() async { + PathProviderPlatform.instance = FakePathProviderPlatform(); + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + test('`onImagePickCallback` should return path of a given file.', () async { + final file = File('test/sample.jpeg'); + final path = await onImagePickCallback(file); + + // Some time must have passed + expect(path == 'test/sample.jpeg', true); + }); + + testWidgets('`webImagePickImpl` should return the URL of the uploaded image on success (200).', (WidgetTester tester) async { + /// We are overriding the `IO` because `readAsBytes` is skipped on tests. + /// We use the mocked file so the test can be executed correctly. + IOOverrides.runZoned( + () async { + // Mocks + final clientMock = MockClient(); + final filePickerMock = MockImageFilePicker(); + + // Set mock behaviour for `filePickerMock` with jpeg magic number byte array https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5 + final listMockFiles = [ + PlatformFile(name: 'image.png', size: 200, path: 'some_path', bytes: Uint8List.fromList([0xff, 0xd8, 0xff, 0xe0])), + ]; + + // Set mock behaviour for image returned from the picker + when(filePickerMock.pickImage()).thenAnswer((_) async => Future.value(FilePickerResult(listMockFiles))); + + // Set mock behaviour for `requestMock` + const body = '{"url":"return_url"}'; + final bodyBytes = utf8.encode(body); + when(clientMock.send(any)).thenAnswer((_) async => http.StreamedResponse(Stream>.fromIterable([bodyBytes]), 200)); + + // With the request being "200", we should expect the return URL be the same as the one defined in the mock body. + final urlResponse = await webImagePickImpl( + clientMock, + filePickerMock, + (file) => Future.value(''), + ); + + expect(urlResponse == 'return_url', true); + }, + createFile: (_) => FileMock(), + ); + }); +} diff --git a/test/unit/image_callbacks_test.mocks.dart b/test/unit/image_callbacks_test.mocks.dart new file mode 100644 index 00000000..c352fd64 --- /dev/null +++ b/test/unit/image_callbacks_test.mocks.dart @@ -0,0 +1,997 @@ +// Mocks generated by Mockito 5.4.2 from annotations +// in dwyl_app/test/unit/image_callbacks_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; +import 'dart:convert' as _i5; +import 'dart:io' as _i3; +import 'dart:typed_data' as _i6; + +import 'package:dwyl_app/presentation/widgets/editor/image_callbacks.dart' + as _i7; +import 'package:file_picker/file_picker.dart' as _i8; +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFile_2 extends _i1.SmartFake implements _i3.File { + _FakeFile_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUri_3 extends _i1.SmartFake implements Uri { + _FakeUri_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDirectory_4 extends _i1.SmartFake implements _i3.Directory { + _FakeDirectory_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDateTime_5 extends _i1.SmartFake implements DateTime { + _FakeDateTime_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRandomAccessFile_6 extends _i1.SmartFake + implements _i3.RandomAccessFile { + _FakeRandomAccessFile_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIOSink_7 extends _i1.SmartFake implements _i3.IOSink { + _FakeIOSink_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFileStat_8 extends _i1.SmartFake implements _i3.FileStat { + _FakeFileStat_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFileSystemEntity_9 extends _i1.SmartFake + implements _i3.FileSystemEntity { + _FakeFileSystemEntity_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i5.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i5.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i5.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i5.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i4.Future<_i2.Response>); + + @override + _i4.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i4.Future.value(''), + ) as _i4.Future); + + @override + _i4.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i4.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i4.Future<_i6.Uint8List>); + + @override + _i4.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i4.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i4.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [ImageFilePicker]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockImageFilePicker extends _i1.Mock implements _i7.ImageFilePicker { + MockImageFilePicker() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future<_i8.FilePickerResult?> pickImage() => (super.noSuchMethod( + Invocation.method( + #pickImage, + [], + ), + returnValue: _i4.Future<_i8.FilePickerResult?>.value(), + ) as _i4.Future<_i8.FilePickerResult?>); +} + +/// A class which mocks [File]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFile extends _i1.Mock implements _i3.File { + MockFile() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.File get absolute => (super.noSuchMethod( + Invocation.getter(#absolute), + returnValue: _FakeFile_2( + this, + Invocation.getter(#absolute), + ), + ) as _i3.File); + + @override + String get path => (super.noSuchMethod( + Invocation.getter(#path), + returnValue: '', + ) as String); + + @override + Uri get uri => (super.noSuchMethod( + Invocation.getter(#uri), + returnValue: _FakeUri_3( + this, + Invocation.getter(#uri), + ), + ) as Uri); + + @override + bool get isAbsolute => (super.noSuchMethod( + Invocation.getter(#isAbsolute), + returnValue: false, + ) as bool); + + @override + _i3.Directory get parent => (super.noSuchMethod( + Invocation.getter(#parent), + returnValue: _FakeDirectory_4( + this, + Invocation.getter(#parent), + ), + ) as _i3.Directory); + + @override + _i4.Future<_i3.File> create({ + bool? recursive = false, + bool? exclusive = false, + }) => + (super.noSuchMethod( + Invocation.method( + #create, + [], + { + #recursive: recursive, + #exclusive: exclusive, + }, + ), + returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + this, + Invocation.method( + #create, + [], + { + #recursive: recursive, + #exclusive: exclusive, + }, + ), + )), + ) as _i4.Future<_i3.File>); + + @override + void createSync({ + bool? recursive = false, + bool? exclusive = false, + }) => + super.noSuchMethod( + Invocation.method( + #createSync, + [], + { + #recursive: recursive, + #exclusive: exclusive, + }, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future<_i3.File> rename(String? newPath) => (super.noSuchMethod( + Invocation.method( + #rename, + [newPath], + ), + returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + this, + Invocation.method( + #rename, + [newPath], + ), + )), + ) as _i4.Future<_i3.File>); + + @override + _i3.File renameSync(String? newPath) => (super.noSuchMethod( + Invocation.method( + #renameSync, + [newPath], + ), + returnValue: _FakeFile_2( + this, + Invocation.method( + #renameSync, + [newPath], + ), + ), + ) as _i3.File); + + @override + _i4.Future<_i3.File> copy(String? newPath) => (super.noSuchMethod( + Invocation.method( + #copy, + [newPath], + ), + returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + this, + Invocation.method( + #copy, + [newPath], + ), + )), + ) as _i4.Future<_i3.File>); + + @override + _i3.File copySync(String? newPath) => (super.noSuchMethod( + Invocation.method( + #copySync, + [newPath], + ), + returnValue: _FakeFile_2( + this, + Invocation.method( + #copySync, + [newPath], + ), + ), + ) as _i3.File); + + @override + _i4.Future length() => (super.noSuchMethod( + Invocation.method( + #length, + [], + ), + returnValue: _i4.Future.value(0), + ) as _i4.Future); + + @override + int lengthSync() => (super.noSuchMethod( + Invocation.method( + #lengthSync, + [], + ), + returnValue: 0, + ) as int); + + @override + _i4.Future lastAccessed() => (super.noSuchMethod( + Invocation.method( + #lastAccessed, + [], + ), + returnValue: _i4.Future.value(_FakeDateTime_5( + this, + Invocation.method( + #lastAccessed, + [], + ), + )), + ) as _i4.Future); + + @override + DateTime lastAccessedSync() => (super.noSuchMethod( + Invocation.method( + #lastAccessedSync, + [], + ), + returnValue: _FakeDateTime_5( + this, + Invocation.method( + #lastAccessedSync, + [], + ), + ), + ) as DateTime); + + @override + _i4.Future setLastAccessed(DateTime? time) => (super.noSuchMethod( + Invocation.method( + #setLastAccessed, + [time], + ), + returnValue: _i4.Future.value(), + ) as _i4.Future); + + @override + void setLastAccessedSync(DateTime? time) => super.noSuchMethod( + Invocation.method( + #setLastAccessedSync, + [time], + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future lastModified() => (super.noSuchMethod( + Invocation.method( + #lastModified, + [], + ), + returnValue: _i4.Future.value(_FakeDateTime_5( + this, + Invocation.method( + #lastModified, + [], + ), + )), + ) as _i4.Future); + + @override + DateTime lastModifiedSync() => (super.noSuchMethod( + Invocation.method( + #lastModifiedSync, + [], + ), + returnValue: _FakeDateTime_5( + this, + Invocation.method( + #lastModifiedSync, + [], + ), + ), + ) as DateTime); + + @override + _i4.Future setLastModified(DateTime? time) => (super.noSuchMethod( + Invocation.method( + #setLastModified, + [time], + ), + returnValue: _i4.Future.value(), + ) as _i4.Future); + + @override + void setLastModifiedSync(DateTime? time) => super.noSuchMethod( + Invocation.method( + #setLastModifiedSync, + [time], + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future<_i3.RandomAccessFile> open( + {_i3.FileMode? mode = _i3.FileMode.read}) => + (super.noSuchMethod( + Invocation.method( + #open, + [], + {#mode: mode}, + ), + returnValue: + _i4.Future<_i3.RandomAccessFile>.value(_FakeRandomAccessFile_6( + this, + Invocation.method( + #open, + [], + {#mode: mode}, + ), + )), + ) as _i4.Future<_i3.RandomAccessFile>); + + @override + _i3.RandomAccessFile openSync({_i3.FileMode? mode = _i3.FileMode.read}) => + (super.noSuchMethod( + Invocation.method( + #openSync, + [], + {#mode: mode}, + ), + returnValue: _FakeRandomAccessFile_6( + this, + Invocation.method( + #openSync, + [], + {#mode: mode}, + ), + ), + ) as _i3.RandomAccessFile); + + @override + _i4.Stream> openRead([ + int? start, + int? end, + ]) => + (super.noSuchMethod( + Invocation.method( + #openRead, + [ + start, + end, + ], + ), + returnValue: _i4.Stream>.empty(), + ) as _i4.Stream>); + + @override + _i3.IOSink openWrite({ + _i3.FileMode? mode = _i3.FileMode.write, + _i5.Encoding? encoding = const _i5.Utf8Codec(), + }) => + (super.noSuchMethod( + Invocation.method( + #openWrite, + [], + { + #mode: mode, + #encoding: encoding, + }, + ), + returnValue: _FakeIOSink_7( + this, + Invocation.method( + #openWrite, + [], + { + #mode: mode, + #encoding: encoding, + }, + ), + ), + ) as _i3.IOSink); + + @override + _i4.Future<_i6.Uint8List> readAsBytes() => (super.noSuchMethod( + Invocation.method( + #readAsBytes, + [], + ), + returnValue: _i4.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i4.Future<_i6.Uint8List>); + + @override + _i6.Uint8List readAsBytesSync() => (super.noSuchMethod( + Invocation.method( + #readAsBytesSync, + [], + ), + returnValue: _i6.Uint8List(0), + ) as _i6.Uint8List); + + @override + _i4.Future readAsString( + {_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + (super.noSuchMethod( + Invocation.method( + #readAsString, + [], + {#encoding: encoding}, + ), + returnValue: _i4.Future.value(''), + ) as _i4.Future); + + @override + String readAsStringSync({_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + (super.noSuchMethod( + Invocation.method( + #readAsStringSync, + [], + {#encoding: encoding}, + ), + returnValue: '', + ) as String); + + @override + _i4.Future> readAsLines( + {_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + (super.noSuchMethod( + Invocation.method( + #readAsLines, + [], + {#encoding: encoding}, + ), + returnValue: _i4.Future>.value([]), + ) as _i4.Future>); + + @override + List readAsLinesSync( + {_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + (super.noSuchMethod( + Invocation.method( + #readAsLinesSync, + [], + {#encoding: encoding}, + ), + returnValue: [], + ) as List); + + @override + _i4.Future<_i3.File> writeAsBytes( + List? bytes, { + _i3.FileMode? mode = _i3.FileMode.write, + bool? flush = false, + }) => + (super.noSuchMethod( + Invocation.method( + #writeAsBytes, + [bytes], + { + #mode: mode, + #flush: flush, + }, + ), + returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + this, + Invocation.method( + #writeAsBytes, + [bytes], + { + #mode: mode, + #flush: flush, + }, + ), + )), + ) as _i4.Future<_i3.File>); + + @override + void writeAsBytesSync( + List? bytes, { + _i3.FileMode? mode = _i3.FileMode.write, + bool? flush = false, + }) => + super.noSuchMethod( + Invocation.method( + #writeAsBytesSync, + [bytes], + { + #mode: mode, + #flush: flush, + }, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future<_i3.File> writeAsString( + String? contents, { + _i3.FileMode? mode = _i3.FileMode.write, + _i5.Encoding? encoding = const _i5.Utf8Codec(), + bool? flush = false, + }) => + (super.noSuchMethod( + Invocation.method( + #writeAsString, + [contents], + { + #mode: mode, + #encoding: encoding, + #flush: flush, + }, + ), + returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + this, + Invocation.method( + #writeAsString, + [contents], + { + #mode: mode, + #encoding: encoding, + #flush: flush, + }, + ), + )), + ) as _i4.Future<_i3.File>); + + @override + void writeAsStringSync( + String? contents, { + _i3.FileMode? mode = _i3.FileMode.write, + _i5.Encoding? encoding = const _i5.Utf8Codec(), + bool? flush = false, + }) => + super.noSuchMethod( + Invocation.method( + #writeAsStringSync, + [contents], + { + #mode: mode, + #encoding: encoding, + #flush: flush, + }, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future exists() => (super.noSuchMethod( + Invocation.method( + #exists, + [], + ), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + + @override + bool existsSync() => (super.noSuchMethod( + Invocation.method( + #existsSync, + [], + ), + returnValue: false, + ) as bool); + + @override + _i4.Future resolveSymbolicLinks() => (super.noSuchMethod( + Invocation.method( + #resolveSymbolicLinks, + [], + ), + returnValue: _i4.Future.value(''), + ) as _i4.Future); + + @override + String resolveSymbolicLinksSync() => (super.noSuchMethod( + Invocation.method( + #resolveSymbolicLinksSync, + [], + ), + returnValue: '', + ) as String); + + @override + _i4.Future<_i3.FileStat> stat() => (super.noSuchMethod( + Invocation.method( + #stat, + [], + ), + returnValue: _i4.Future<_i3.FileStat>.value(_FakeFileStat_8( + this, + Invocation.method( + #stat, + [], + ), + )), + ) as _i4.Future<_i3.FileStat>); + + @override + _i3.FileStat statSync() => (super.noSuchMethod( + Invocation.method( + #statSync, + [], + ), + returnValue: _FakeFileStat_8( + this, + Invocation.method( + #statSync, + [], + ), + ), + ) as _i3.FileStat); + + @override + _i4.Future<_i3.FileSystemEntity> delete({bool? recursive = false}) => + (super.noSuchMethod( + Invocation.method( + #delete, + [], + {#recursive: recursive}, + ), + returnValue: + _i4.Future<_i3.FileSystemEntity>.value(_FakeFileSystemEntity_9( + this, + Invocation.method( + #delete, + [], + {#recursive: recursive}, + ), + )), + ) as _i4.Future<_i3.FileSystemEntity>); + + @override + void deleteSync({bool? recursive = false}) => super.noSuchMethod( + Invocation.method( + #deleteSync, + [], + {#recursive: recursive}, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Stream<_i3.FileSystemEvent> watch({ + int? events = 15, + bool? recursive = false, + }) => + (super.noSuchMethod( + Invocation.method( + #watch, + [], + { + #events: events, + #recursive: recursive, + }, + ), + returnValue: _i4.Stream<_i3.FileSystemEvent>.empty(), + ) as _i4.Stream<_i3.FileSystemEvent>); +} diff --git a/test/widget/emoji_widget_test.dart b/test/widget/emoji_widget_test.dart index 86493f80..ca4d32f2 100644 --- a/test/widget/emoji_widget_test.dart +++ b/test/widget/emoji_widget_test.dart @@ -1,11 +1,7 @@ import 'package:dwyl_app/blocs/blocs.dart'; -import 'package:dwyl_app/main.dart'; -import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; -import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:responsive_framework/responsive_framework.dart'; From bef0bf5cd69ca72648ec883ef89ce3ee50347531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 4 Oct 2023 21:21:54 +0100 Subject: [PATCH 31/43] fix: Image picking tests fix. #275 --- pubspec.lock | 2 +- pubspec.yaml | 2 + test/integration_test.dart | 81 +++++++++++++++++++++++++++-- test/unit/image_callbacks_test.dart | 6 +-- 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 66bb96ab..1f3746fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -624,7 +624,7 @@ packages: source: hosted version: "0.2.1+1" image_picker_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: image_picker_platform_interface sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 diff --git a/pubspec.yaml b/pubspec.yaml index bdc7701c..0a597563 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,9 +50,11 @@ dev_dependencies: cross_file: ^0.3.3+5 path_provider_platform_interface: ^2.1.1 + image_picker_platform_interface: ^2.9.1 flutter: uses-material-design: true assets: - assets/icon/ + - assets/ diff --git a/test/integration_test.dart b/test/integration_test.dart index d306dabc..66667f1a 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -3,11 +3,49 @@ import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill_test.dart'; import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:dwyl_app/main.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_io/io.dart'; + +class FakeImagePicker extends ImagePickerPlatform { + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + final data = await rootBundle.load('assets/sample.jpeg'); + final bytes = data.buffer.asUint8List(); + final tempDir = await getTemporaryDirectory(); + final file = await File( + '${tempDir.path}/doc.png', + ).writeAsBytes(bytes); + + return XFile(file.path); + } + + @override + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final data = await rootBundle.load('assets/sample.jpeg'); + final bytes = data.buffer.asUint8List(); + final tempDir = await getTemporaryDirectory(); + final file = await File( + '${tempDir.path}/sample.png', + ).writeAsBytes(bytes); + return [ + XFile( + file.path, + ), + ]; + } +} void main() { /// Bootstraps a sample main application, whether it [isWeb] or not. @@ -21,8 +59,10 @@ void main() { ); } + // See https://stackoverflow.com/questions/76586920/mocking-imagepicker-in-flutter-integration-tests-not-working for context. setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); + ImagePickerPlatform.instance = FakeImagePicker(); }); group('Normal build', () { @@ -403,9 +443,6 @@ void main() { group('Image picker', () { testWidgets('should show and select image', (WidgetTester tester) async { - // Mock image - // mockImagePicker(tester); - // Build our app and trigger a frame. final app = initializeMainApp(isWeb: false); await tester.pumpWidget(app); @@ -426,12 +463,46 @@ void main() { await tester.pump(const Duration(minutes: 1)); // Press image button + // Because of the override, should embed image. + final imageButton = find.byType(ImageButton); + await tester.tap(imageButton); + await tester.pumpAndSettle(); + + // Image correctly added, editor should be visible again. + expect(editor.hitTestable(), findsOneWidget); + }); + + + // TODO does not work because inside ImageButton, there is a `kIsWeb`. + // This can't be mockable, unless we use a custom button to recreate this behaviour. + testWidgets('should show and select image (on the web - uploads image to the web)', (WidgetTester tester) async { + // Build our app and trigger a frame. + final app = initializeMainApp(isWeb: true); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // Tap textfield to open new page to create item + await tester.tap(find.byKey(textfieldKey)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Should show editor and toolbar + final editor = find.byType(QuillEditor); + final toolbar = find.byType(QuillToolbar); + expect(editor.hitTestable(), findsOneWidget); + expect(toolbar.hitTestable(), findsOneWidget); + + // Drag toolbar to the right + await tester.drag(toolbar, const Offset(-500, 0)); + await tester.pump(const Duration(minutes: 1)); + + // Press image button + // Because of the override, should embed image. final imageButton = find.byType(ImageButton); await tester.tap(imageButton); await tester.pumpAndSettle(); - // TODO can't choose image. - // Check https://github.com/singerdmx/flutter-quill/issues/1389. + // Image correctly added, editor should be visible again. + expect(editor.hitTestable(), findsOneWidget); }); }); diff --git a/test/unit/image_callbacks_test.dart b/test/unit/image_callbacks_test.dart index bbe71f92..fea41cb9 100644 --- a/test/unit/image_callbacks_test.dart +++ b/test/unit/image_callbacks_test.dart @@ -17,7 +17,7 @@ class FakePathProviderPlatform extends PathProviderPlatform implements Fake { @override Future getApplicationDocumentsPath() async { // Make sure is the same folder used in the tests. - return 'test'; + return 'assets'; } } @@ -45,11 +45,11 @@ void main() { }); test('`onImagePickCallback` should return path of a given file.', () async { - final file = File('test/sample.jpeg'); + final file = File('assets/sample.jpeg'); final path = await onImagePickCallback(file); // Some time must have passed - expect(path == 'test/sample.jpeg', true); + expect(path == 'assets/sample.jpeg', true); }); testWidgets('`webImagePickImpl` should return the URL of the uploaded image on success (200).', (WidgetTester tester) async { From d9450d45d86d20cabfd765ee4e029d59182dc64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 4 Oct 2023 23:25:48 +0100 Subject: [PATCH 32/43] fix: FIxing item label to show. #275 --- assets/sample.jpeg | Bin 0 -> 18625 bytes ios/Podfile.lock | 6 ++++++ lib/presentation/widgets/items.dart | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 assets/sample.jpeg diff --git a/assets/sample.jpeg b/assets/sample.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..24c6d9e75bfe24b3a080874e7f566b92a9616366 GIT binary patch literal 18625 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 I>jg9a*|vz~1poj5 literal 0 HcmV?d00001 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bacf168a..34f9741b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -51,6 +51,8 @@ PODS: - Flutter - image_picker_ios (0.0.1): - Flutter + - integration_test (0.0.1): + - Flutter - OrderedSet (5.0.0) - pasteboard (0.0.1): - Flutter @@ -78,6 +80,7 @@ DEPENDENCIES: - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - gallery_saver (from `.symlinks/plugins/gallery_saver/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -109,6 +112,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/gallery_saver/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" pasteboard: :path: ".symlinks/plugins/pasteboard/ios" path_provider_foundation: @@ -131,6 +136,7 @@ SPEC CHECKSUMS: flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 gallery_saver: 9fc173c9f4fcc48af53b2a9ebea1b643255be542 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + integration_test: 13825b8a9334a850581300559b8839134b124670 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 diff --git a/lib/presentation/widgets/items.dart b/lib/presentation/widgets/items.dart index fbe2b17c..a4cfe292 100644 --- a/lib/presentation/widgets/items.dart +++ b/lib/presentation/widgets/items.dart @@ -105,6 +105,9 @@ class _ItemCardState extends State { final checkboxSize = deviceWidth > 425.0 ? 30.0 : 20.0; + // Text shown in the item + final textToShow = widget.item.description.split('\n').first; + return Container( key: itemCardWidgetKey, constraints: const BoxConstraints(minHeight: 70), @@ -155,7 +158,7 @@ class _ItemCardState extends State { // On mobile if (ResponsiveBreakpoints.of(context).isMobile) { return Text( - widget.item.description, + textToShow, style: TextStyle( fontSize: 20, decoration: widget.item.completed ? TextDecoration.lineThrough : TextDecoration.none, @@ -168,7 +171,7 @@ class _ItemCardState extends State { // On tablet and up else { return Text( - widget.item.description, + textToShow, style: TextStyle( fontSize: 25, decoration: widget.item.completed ? TextDecoration.lineThrough : TextDecoration.none, From 38954cc6f036ede2673ad694de084539dd39571e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 4 Oct 2023 23:37:28 +0100 Subject: [PATCH 33/43] fix: Fix navigation going to black screen bug. #275 --- lib/presentation/widgets/items.dart | 7 +++---- lib/presentation/widgets/navbar.dart | 12 +++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/presentation/widgets/items.dart b/lib/presentation/widgets/items.dart index a4cfe292..961608de 100644 --- a/lib/presentation/widgets/items.dart +++ b/lib/presentation/widgets/items.dart @@ -101,13 +101,12 @@ class _ItemCardState extends State { @override Widget build(BuildContext context) { - final deviceWidth = MediaQuery.of(context).size.width; - - final checkboxSize = deviceWidth > 425.0 ? 30.0 : 20.0; - // Text shown in the item final textToShow = widget.item.description.split('\n').first; + // Checkbox size according to view + final checkboxSize = ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET) ? 30.0 : 20.0; + return Container( key: itemCardWidgetKey, constraints: const BoxConstraints(minHeight: 70), diff --git a/lib/presentation/widgets/navbar.dart b/lib/presentation/widgets/navbar.dart index f8c60c69..d045e983 100644 --- a/lib/presentation/widgets/navbar.dart +++ b/lib/presentation/widgets/navbar.dart @@ -8,14 +8,17 @@ const logoKey = Key('logoKey'); class NavBar extends StatelessWidget implements PreferredSizeWidget { /// Boolean that tells the bar to have a button to go to the previous page final bool showGoBackButton; + /// Build context for the "go back" button works final BuildContext givenContext; + /// Callback for when the user taps on the navbar final VoidCallback? onTap; const NavBar({ - required this.givenContext, super.key, - this.showGoBackButton = false, + required this.givenContext, + super.key, + this.showGoBackButton = false, this.onTap, }); @@ -30,7 +33,10 @@ class NavBar extends StatelessWidget implements PreferredSizeWidget { children: [ GestureDetector( onTap: () { - Navigator.pop(givenContext); + // Check if we can pop and are not at the end of the stack. + if (Navigator.of(context).canPop()) { + Navigator.pop(givenContext); + } }, child: // dwyl logo From 8c30437cb0a45e08a5a16e2251b0e8599e94dbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 4 Oct 2023 23:48:29 +0100 Subject: [PATCH 34/43] fix: Fixing navigation bug. #275 --- lib/presentation/views/new_item.dart | 136 +++++++++++++-------------- lib/presentation/widgets/navbar.dart | 5 +- 2 files changed, 68 insertions(+), 73 deletions(-) diff --git a/lib/presentation/views/new_item.dart b/lib/presentation/views/new_item.dart index 10f0ab5f..3e385722 100644 --- a/lib/presentation/views/new_item.dart +++ b/lib/presentation/views/new_item.dart @@ -53,82 +53,80 @@ class _NewItemPageState extends State { Widget build(BuildContext context) { final isWeb = BlocProvider.of(context).state.isWeb; - return MaterialApp( - home: Scaffold( - appBar: NavBar( - key: newItemPageNavbarKey, - givenContext: context, - showGoBackButton: true, - onTap: () => _onTapNavbar(context), - ), - body: SafeArea( - child: Column( - children: [ - // Textfield that is expanded and borderless - Expanded( - child: DeltaTodoEditor( - isWeb: isWeb, - editorController: _controller, - ), + return Scaffold( + appBar: NavBar( + key: newItemPageNavbarKey, + givenContext: context, + showGoBackButton: true, + onTap: () => _onTapNavbar(context), + ), + body: SafeArea( + child: Column( + children: [ + // Textfield that is expanded and borderless + Expanded( + child: DeltaTodoEditor( + isWeb: isWeb, + editorController: _controller, ), - - // Save button. - // When submitted, it adds a new item, clears the controller and navigates back - Align( - alignment: Alignment.bottomRight, - child: ElevatedButton( - key: saveButtonKey, - style: ElevatedButton.styleFrom( - backgroundColor: const Color.fromARGB(255, 75, 192, 169), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), + ), + + // Save button. + // When submitted, it adds a new item, clears the controller and navigates back + Align( + alignment: Alignment.bottomRight, + child: ElevatedButton( + key: saveButtonKey, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 75, 192, 169), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, ), - onPressed: () { - final document = _controller.document; - var text = document.toPlainText(); - - // Remove last newline from text (document.toPlainText() adds this at the end of the text) - if (text.isNotEmpty) { - final lastChar = text[text.length - 1]; - text = lastChar == '\n' ? text.substring(0, text.length - 1) : text; + ), + onPressed: () { + final document = _controller.document; + var text = document.toPlainText(); + + // Remove last newline from text (document.toPlainText() adds this at the end of the text) + if (text.isNotEmpty) { + final lastChar = text[text.length - 1]; + text = lastChar == '\n' ? text.substring(0, text.length - 1) : text; + } + + if (text.isNotEmpty) { + // Create new item and create AddTodo event + final newTodoItem = Item(description: text, document: document); + BlocProvider.of(context).add(AddItemEvent(newTodoItem)); + + // Clear textfield + _controller.clear(); + + // Go back to home page + Navigator.pop(context); + } + }, + child: SizedBox( + child: (() { + // On mobile + if (ResponsiveBreakpoints.of(context).isMobile) { + return const Text( + 'Save', + style: TextStyle(fontSize: 24), + ); } - if (text.isNotEmpty) { - // Create new item and create AddTodo event - final newTodoItem = Item(description: text, document: document); - BlocProvider.of(context).add(AddItemEvent(newTodoItem)); - - // Clear textfield - _controller.clear(); - - // Go back to home page - Navigator.pop(context); + // On tablet and up + else { + return const Text( + 'Save', + style: TextStyle(fontSize: 40), + ); } - }, - child: SizedBox( - child: (() { - // On mobile - if (ResponsiveBreakpoints.of(context).isMobile) { - return const Text( - 'Save', - style: TextStyle(fontSize: 24), - ); - } - - // On tablet and up - else { - return const Text( - 'Save', - style: TextStyle(fontSize: 40), - ); - } - }()), - ), + }()), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/presentation/widgets/navbar.dart b/lib/presentation/widgets/navbar.dart index d045e983..4d6df1b7 100644 --- a/lib/presentation/widgets/navbar.dart +++ b/lib/presentation/widgets/navbar.dart @@ -33,10 +33,7 @@ class NavBar extends StatelessWidget implements PreferredSizeWidget { children: [ GestureDetector( onTap: () { - // Check if we can pop and are not at the end of the stack. - if (Navigator.of(context).canPop()) { - Navigator.pop(givenContext); - } + Navigator.of(givenContext).maybePop(); }, child: // dwyl logo From 9cd36aadd8308df5c4b58e02742068acce6cc854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 4 Oct 2023 23:49:05 +0100 Subject: [PATCH 35/43] chore: Add simple comment on navigation bug fix. #275 --- lib/presentation/widgets/navbar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/widgets/navbar.dart b/lib/presentation/widgets/navbar.dart index 4d6df1b7..40cb2511 100644 --- a/lib/presentation/widgets/navbar.dart +++ b/lib/presentation/widgets/navbar.dart @@ -33,6 +33,7 @@ class NavBar extends StatelessWidget implements PreferredSizeWidget { children: [ GestureDetector( onTap: () { + // https://stackoverflow.com/questions/53723294/flutter-navigator-popcontext-returning-a-black-screen Navigator.of(givenContext).maybePop(); }, child: From cada373e0439ce7ca46bcfc615a59bde1fae3930 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Thu, 5 Oct 2023 04:01:36 +0100 Subject: [PATCH 36/43] hyper link build button in README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c7232d5c..0d14429c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # `app` -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/app/ci.yml?label=build&style=flat-square&branch=main) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/app/ci.yml?label=build&style=flat-square&branch=main)](https://github.com/dwyl/app/actions/workflows/ci.yml) [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/app/main.svg?style=flat-square)](https://codecov.io/github/dwyl/app?branch=main) [![contributions welcome](https://img.shields.io/badge/feedback-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/app/issues) [![HitCount](https://hits.dwyl.com/dwyl/app.svg)](https://hits.dwyl.com/dwyl/app) @@ -116,7 +116,6 @@ To run this app you will need to have ***PostgreSQL Installed and Running*** on ***before*** you attempt to run this example. > see: https://wiki.postgresql.org/wiki/Detailed_installation_guides ---> ### No Registration Required From 7bed7e6a1874d1f1a369e8f7c8ad94d204e99a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 9 Oct 2023 13:12:42 +0100 Subject: [PATCH 37/43] feat: Adding Data Layer and models. Adding Providers and Repositories. Adding error models. Adding request models. Adding network utilities (Either and Left/Right) to better return models. --- lib/data/providers/image_provider.dart | 40 +++++++++++++++++++ lib/data/providers/providers.dart | 1 + .../repositories/image/image_repository.dart | 20 ++++++++++ lib/data/repositories/repositories.dart | 1 + lib/models/errors/errors.dart | 1 + lib/models/errors/request_error.dart | 8 ++++ lib/models/models.dart | 1 + lib/presentation/presentation.dart | 2 + .../widgets/editor/image_callbacks.dart | 2 +- lib/presentation/widgets/widgets.dart | 1 + pubspec.lock | 8 ++++ pubspec.yaml | 1 + 12 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 lib/data/providers/image_provider.dart create mode 100644 lib/data/providers/providers.dart create mode 100644 lib/data/repositories/image/image_repository.dart create mode 100644 lib/data/repositories/repositories.dart create mode 100644 lib/models/errors/errors.dart create mode 100644 lib/models/errors/request_error.dart create mode 100644 lib/presentation/presentation.dart diff --git a/lib/data/providers/image_provider.dart b/lib/data/providers/image_provider.dart new file mode 100644 index 00000000..8e3c174a --- /dev/null +++ b/lib/data/providers/image_provider.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:dartz/dartz.dart'; +import 'package:dwyl_app/models/models.dart'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'package:mime/mime.dart'; + +const imageEndpoint = 'http://localhost:4000/api/images'; + +/// Image data provider. +/// Meant to be invoked by the domain layer (repositories). +/// +/// It will call the appropriate URL (in this case, it's `imgup`) to mainly upload images. +class ImageProvider { + /// HTTP client used to make requests. + final http.Client client; + + ImageProvider({required this.client}); + + /// Uploads an image (as an array of [bytes]) to https://imgup.fly.dev/ and returns the appropriate response. + Future> uploadImage(Uint8List bytes, String filename) async { + final request = http.MultipartRequest('POST', Uri.parse(imageEndpoint)); + + final httpImage = + http.MultipartFile.fromBytes('image', bytes, contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), filename: filename); + request.files.add(httpImage); + + // Check the response and handle accordingly + return client.send(request).then((response) async { + if (response.statusCode != 200) { + return Left(RequestError(code: response.statusCode, description: response.toString())); + } + + final responseStream = await http.Response.fromStream(response); + final responseData = json.decode(responseStream.body); + return Right(responseData['url']); + }); + } +} diff --git a/lib/data/providers/providers.dart b/lib/data/providers/providers.dart new file mode 100644 index 00000000..ae91df5a --- /dev/null +++ b/lib/data/providers/providers.dart @@ -0,0 +1 @@ +export 'image_provider.dart'; \ No newline at end of file diff --git a/lib/data/repositories/image/image_repository.dart b/lib/data/repositories/image/image_repository.dart new file mode 100644 index 00000000..d15c6881 --- /dev/null +++ b/lib/data/repositories/image/image_repository.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; + +import 'package:dartz/dartz.dart'; +import 'package:dwyl_app/data/providers/providers.dart' as providers; +import 'package:dwyl_app/models/models.dart'; +import 'package:http/http.dart' as http; + +abstract class ImageRepository { + Future> uploadImage(Uint8List bytes, String filename); +} + +/// Image repository (part of Domain Layer) +/// Meant to be invoked by the application layer (presentation and blocs), +/// it will call the appropriate Image Data Provider to deal with images. +class ImgupRepository implements ImageRepository { + @override + Future> uploadImage(Uint8List bytes, String filename) async { + return providers.ImageProvider(client: http.Client()).uploadImage(bytes, filename); + } +} diff --git a/lib/data/repositories/repositories.dart b/lib/data/repositories/repositories.dart new file mode 100644 index 00000000..36200bff --- /dev/null +++ b/lib/data/repositories/repositories.dart @@ -0,0 +1 @@ +export 'image/image_repository.dart'; diff --git a/lib/models/errors/errors.dart b/lib/models/errors/errors.dart new file mode 100644 index 00000000..65a0478b --- /dev/null +++ b/lib/models/errors/errors.dart @@ -0,0 +1 @@ +export 'request_error.dart'; diff --git a/lib/models/errors/request_error.dart b/lib/models/errors/request_error.dart new file mode 100644 index 00000000..96ebd773 --- /dev/null +++ b/lib/models/errors/request_error.dart @@ -0,0 +1,8 @@ +/// Simple class pertaining to a given request error. +/// Has the error HTTP [code] and a [description] of the error occured. +class RequestError { + final int code; + final String description; + + RequestError({required this.code, required this.description}); +} diff --git a/lib/models/models.dart b/lib/models/models.dart index d9a6700f..0594d85c 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -1,2 +1,3 @@ export 'item.dart'; export 'stopwatch.dart'; +export 'errors/errors.dart'; diff --git a/lib/presentation/presentation.dart b/lib/presentation/presentation.dart new file mode 100644 index 00000000..e9f1deb1 --- /dev/null +++ b/lib/presentation/presentation.dart @@ -0,0 +1,2 @@ +export 'views/views.dart'; +export 'widgets/widgets.dart'; \ No newline at end of file diff --git a/lib/presentation/widgets/editor/image_callbacks.dart b/lib/presentation/widgets/editor/image_callbacks.dart index 0a0c476d..a801fbd3 100644 --- a/lib/presentation/widgets/editor/image_callbacks.dart +++ b/lib/presentation/widgets/editor/image_callbacks.dart @@ -11,7 +11,7 @@ import 'package:mime/mime.dart'; import 'package:http_parser/http_parser.dart'; /// The URL endpoint in which the images will be uploaded and hosted. -const apiEndpointURL = 'https://imgup.fly.dev/api/images'; +const apiEndpointURL = 'http://localhost:4000/api/images'; /// Receives a file [file], copies it to the app's documents directory and returns the path of the copied file. Future onImagePickCallback(File file) async { diff --git a/lib/presentation/widgets/widgets.dart b/lib/presentation/widgets/widgets.dart index 59419b34..14d33d29 100644 --- a/lib/presentation/widgets/widgets.dart +++ b/lib/presentation/widgets/widgets.dart @@ -1,3 +1,4 @@ export 'items.dart'; export 'navbar.dart'; export 'editor/item_editor.dart'; +export 'editor/image_callbacks.dart'; diff --git a/pubspec.lock b/pubspec.lock index 1f3746fa..5b7b2741 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" device_info_plus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0a597563..cca6e51d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: # Logging lumberdash: ^3.0.0 colorize_lumberdash: ^3.0.0 + dartz: ^0.10.1 dev_dependencies: integration_test: From a826e1ab1105c86f4445b136690ece9f63ca767e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 9 Oct 2023 15:02:26 +0100 Subject: [PATCH 38/43] fix: Adding Data cubit and adding core folder with data layer that is changed according to release mode. --- lib/blocs/data/data_cubit.dart | 9 ++++ lib/blocs/data/data_state.dart | 14 +++++ lib/core/app.dart | 32 +++++++++++ lib/core/core.dart | 2 + lib/core/data_layer.dart | 39 ++++++++++++++ lib/data/data.dart | 2 + .../repositories/image/image_repository.dart | 6 ++- lib/main.dart | 53 +++++++------------ test/integration_test.dart | 2 +- 9 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 lib/blocs/data/data_cubit.dart create mode 100644 lib/blocs/data/data_state.dart create mode 100644 lib/core/app.dart create mode 100644 lib/core/core.dart create mode 100644 lib/core/data_layer.dart create mode 100644 lib/data/data.dart diff --git a/lib/blocs/data/data_cubit.dart b/lib/blocs/data/data_cubit.dart new file mode 100644 index 00000000..20d72c59 --- /dev/null +++ b/lib/blocs/data/data_cubit.dart @@ -0,0 +1,9 @@ +import 'package:bloc/bloc.dart'; +import 'package:dwyl_app/data/repositories/repositories.dart'; +import 'package:equatable/equatable.dart'; + +part 'data_state.dart'; + +class DataCubit extends Cubit { + DataCubit({required ImageRepository imageRepository}) : super(DataInitial(imageRepository)); +} \ No newline at end of file diff --git a/lib/blocs/data/data_state.dart b/lib/blocs/data/data_state.dart new file mode 100644 index 00000000..9b593eea --- /dev/null +++ b/lib/blocs/data/data_state.dart @@ -0,0 +1,14 @@ +part of 'data_cubit.dart'; + +sealed class DataState extends Equatable { + final ImageRepository imageRepository; + + const DataState(this.imageRepository); +} + +final class DataInitial extends DataState { + const DataInitial(super.imageRepository); + + @override + List get props => []; +} diff --git a/lib/core/app.dart b/lib/core/app.dart new file mode 100644 index 00000000..5c1a2368 --- /dev/null +++ b/lib/core/app.dart @@ -0,0 +1,32 @@ +import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/core/data_layer.dart'; +import 'package:dwyl_app/presentation/views/home.dart'; +import 'package:flutter/material.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + + +/// The main class of the app. +/// It will create the state manager with `BlocProvider` and make it available along the widget tree. +/// +/// The `ItemListStarted` event is instantly spawned when the app starts. +/// This is because we've yet have the need to fetch information from third-party APIs before initializing the app. +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: const HomePage(), + builder: (context, child) => ResponsiveBreakpoints.builder( + child: child!, + breakpoints: [ + const Breakpoint(start: 0, end: 425, name: MOBILE), + const Breakpoint(start: 426, end: 768, name: TABLET), + const Breakpoint(start: 769, end: 1024, name: DESKTOP), + const Breakpoint(start: 1025, end: 1440, name: 'LARGE_DESKTOP'), + const Breakpoint(start: 1441, end: double.infinity, name: '4K'), + ], + ), + ); + } +} diff --git a/lib/core/core.dart b/lib/core/core.dart new file mode 100644 index 00000000..3bf8d5c8 --- /dev/null +++ b/lib/core/core.dart @@ -0,0 +1,2 @@ +export 'data_layer.dart'; +export 'app.dart'; diff --git a/lib/core/data_layer.dart b/lib/core/data_layer.dart new file mode 100644 index 00000000..e0df78a1 --- /dev/null +++ b/lib/core/data_layer.dart @@ -0,0 +1,39 @@ +import 'package:dwyl_app/data/repositories/image/image_repository.dart'; +import 'package:http/http.dart' as http; + +/// This class pertains to the Data Layer of the application. +/// It is used to store repositories that are used throughout the app. +class DataLayer { + final _map = {}; + void store(final S value) => _map[S] = value; + T? lookup() => _map[S]; +} + +/// Creates a `DataLayer` object that holds the repositories within the app. +/// +/// `isReleaseMode` is passed to point to real data sources or not. +DataLayer createDataLayer({ + required final bool isRelease, +}) { + final layer = DataLayer(); + + if (isRelease) { + final httpClient = http.Client(); + + layer.store( + ImgupRepository(client: httpClient), + ); + } else { + /* + We can add mock repositories, like so: + + vault.store( + FakeImageRepository() + ); + + Just create a fake repository class that returns mocked values instead of real ones in /lib/data/repositories/image/image_repository.dart + */ + } + + return layer; +} diff --git a/lib/data/data.dart b/lib/data/data.dart new file mode 100644 index 00000000..bc1a8c26 --- /dev/null +++ b/lib/data/data.dart @@ -0,0 +1,2 @@ +export 'providers/providers.dart'; +export 'repositories/repositories.dart'; diff --git a/lib/data/repositories/image/image_repository.dart b/lib/data/repositories/image/image_repository.dart index d15c6881..d8e202a5 100644 --- a/lib/data/repositories/image/image_repository.dart +++ b/lib/data/repositories/image/image_repository.dart @@ -13,8 +13,12 @@ abstract class ImageRepository { /// Meant to be invoked by the application layer (presentation and blocs), /// it will call the appropriate Image Data Provider to deal with images. class ImgupRepository implements ImageRepository { + final http.Client client; + + ImgupRepository({required this.client}); + @override Future> uploadImage(Uint8List bytes, String filename) async { - return providers.ImageProvider(client: http.Client()).uploadImage(bytes, filename); + return providers.ImageProvider(client: client).uploadImage(bytes, filename); } } diff --git a/lib/main.dart b/lib/main.dart index e8911819..d7707615 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,6 @@ +import 'package:dwyl_app/blocs/data/data_cubit.dart'; +import 'package:dwyl_app/core/core.dart'; +import 'package:dwyl_app/data/repositories/repositories.dart'; import 'package:dwyl_app/logging/logging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,40 +15,24 @@ void main() { Bloc.observer = GlobalLogBlocObserver(); putLumberdashToWork(withClients: [ColorizeLumberdash()]); - runApp( - MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), - BlocProvider(create: (context) => AppCubit(isWeb: kIsWeb)), - ], - child: const MainApp(), - ), - ); -} -// coverage:ignore-end + // Creating data layer + final dataLayer = createDataLayer(isRelease: true); -/// The main class of the app. -/// It will create the state manager with `BlocProvider` and make it available along the widget tree. -/// -/// The `ItemListStarted` event is instantly spawned when the app starts. -/// This is because we've yet have the need to fetch information from third-party APIs before initializing the app. -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: const HomePage(), - builder: (context, child) => ResponsiveBreakpoints.builder( - child: child!, - breakpoints: [ - const Breakpoint(start: 0, end: 425, name: MOBILE), - const Breakpoint(start: 426, end: 768, name: TABLET), - const Breakpoint(start: 769, end: 1024, name: DESKTOP), - const Breakpoint(start: 1025, end: 1440, name: 'LARGE_DESKTOP'), - const Breakpoint(start: 1441, end: double.infinity, name: '4K'), + // Wraps the application with: + // - Repository Provider: to access the repositories for the data layer. + // - Bloc Providers: to access the blocs for the application. + runApp( + RepositoryProvider( + create: (context) => dataLayer, + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => ItemBloc()..add(ItemListStarted())), + BlocProvider(create: (context) => AppCubit(isWeb: kIsWeb)), + BlocProvider(create: (context) => DataCubit(imageRepository: dataLayer.lookup() as ImageRepository)), ], + child: const MainApp(), ), - ); - } + ), + ); } +// coverage:ignore-end \ No newline at end of file diff --git a/test/integration_test.dart b/test/integration_test.dart index 66667f1a..44e22f62 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1,4 +1,5 @@ import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/core/core.dart'; import 'package:dwyl_app/presentation/views/views.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:dwyl_app/presentation/widgets/widgets.dart'; @@ -8,7 +9,6 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill_test.dart'; import 'package:flutter_quill_extensions/embeds/toolbar/image_button.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:dwyl_app/main.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_io/io.dart'; From 0e05ccb795b5a9165eb8662c05092994af828e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 9 Oct 2023 18:06:36 +0100 Subject: [PATCH 39/43] fix: Making image repository work and mockable in tests. Fixing tests. --- lib/core/app.dart | 2 - .../repositories/image/image_repository.dart | 4 +- lib/main.dart | 2 - .../widgets/editor/image_callbacks.dart | 35 +- .../widgets/editor/item_editor.dart | 5 +- test/unit/image_callbacks_test.dart | 16 +- test/unit/image_callbacks_test.mocks.dart | 463 +++++------------- 7 files changed, 150 insertions(+), 377 deletions(-) diff --git a/lib/core/app.dart b/lib/core/app.dart index 5c1a2368..9ed569b8 100644 --- a/lib/core/app.dart +++ b/lib/core/app.dart @@ -1,5 +1,3 @@ -import 'package:dwyl_app/blocs/blocs.dart'; -import 'package:dwyl_app/core/data_layer.dart'; import 'package:dwyl_app/presentation/views/home.dart'; import 'package:flutter/material.dart'; import 'package:responsive_framework/responsive_framework.dart'; diff --git a/lib/data/repositories/image/image_repository.dart b/lib/data/repositories/image/image_repository.dart index d8e202a5..575a0476 100644 --- a/lib/data/repositories/image/image_repository.dart +++ b/lib/data/repositories/image/image_repository.dart @@ -6,7 +6,7 @@ import 'package:dwyl_app/models/models.dart'; import 'package:http/http.dart' as http; abstract class ImageRepository { - Future> uploadImage(Uint8List bytes, String filename); + Future> uploadImage(Uint8List bytes, String filename); } /// Image repository (part of Domain Layer) @@ -18,7 +18,7 @@ class ImgupRepository implements ImageRepository { ImgupRepository({required this.client}); @override - Future> uploadImage(Uint8List bytes, String filename) async { + Future> uploadImage(Uint8List bytes, String filename) async { return providers.ImageProvider(client: client).uploadImage(bytes, filename); } } diff --git a/lib/main.dart b/lib/main.dart index d7707615..f22515e0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,10 +4,8 @@ import 'package:dwyl_app/data/repositories/repositories.dart'; import 'package:dwyl_app/logging/logging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:responsive_framework/responsive_framework.dart'; import 'blocs/blocs.dart'; -import 'presentation/views/views.dart'; // coverage:ignore-start void main() { diff --git a/lib/presentation/widgets/editor/image_callbacks.dart b/lib/presentation/widgets/editor/image_callbacks.dart index a801fbd3..9f761553 100644 --- a/lib/presentation/widgets/editor/image_callbacks.dart +++ b/lib/presentation/widgets/editor/image_callbacks.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; +import 'package:dwyl_app/data/repositories/repositories.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:http/http.dart' as http; -import 'package:mime/mime.dart'; -import 'package:http_parser/http_parser.dart'; /// The URL endpoint in which the images will be uploaded and hosted. const apiEndpointURL = 'http://localhost:4000/api/images'; @@ -20,11 +17,11 @@ Future onImagePickCallback(File file) async { return copiedFile.path.toString(); } -/// Opens gallery (on mobile) or file explorer (on web). +/// Opens gallery (on mobile) or file explorer (on web). /// Upon picking an image, it is uploaded and the URL of where the image is hosted is returned. -/// +/// /// Returns `null` if no image was picked or the image was not correctly uploaded. -Future webImagePickImpl(http.Client client, ImageFilePicker filePicker, OnImagePickCallback onImagePickCallback) async { +Future webImagePickImpl(ImageRepository imageRepository, ImageFilePicker filePicker, OnImagePickCallback onImagePickCallback) async { // Lets the user pick one file; files with any file extension can be selected final result = await filePicker.pickImage(); @@ -41,27 +38,11 @@ Future webImagePickImpl(http.Client client, ImageFilePicker filePicker, return null; } - // Make HTTP request to upload the image to the file - final request = http.MultipartRequest('POST', Uri.parse(apiEndpointURL)); - - final httpImage = http.MultipartFile.fromBytes( - 'image', - bytes, - contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), - filename: platformFile.name, + final uploadRet = await imageRepository.uploadImage(bytes, platformFile.name); + return uploadRet.fold( + (error) => null, + (r) => r, ); - request.files.add(httpImage); - - // Check the response and handle accordingly - return client.send(request).then((response) async { - if (response.statusCode != 200) { - return null; - } - - final responseStream = await http.Response.fromStream(response); - final responseData = json.decode(responseStream.body); - return responseData['url']; - }); } // coverage:ignore-start diff --git a/lib/presentation/widgets/editor/item_editor.dart b/lib/presentation/widgets/editor/item_editor.dart index 5b034dfd..7333b47e 100644 --- a/lib/presentation/widgets/editor/item_editor.dart +++ b/lib/presentation/widgets/editor/item_editor.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'dart:ui'; +import 'package:dwyl_app/blocs/blocs.dart'; +import 'package:dwyl_app/blocs/data/data_cubit.dart'; import 'package:dwyl_app/presentation/widgets/editor/emoji_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; -import 'package:http/http.dart' as http; import 'image_callbacks.dart'; import 'web_embeds/web_embeds.dart'; @@ -180,7 +181,7 @@ class DeltaTodoEditorState extends State { onImagePickCallback: onImagePickCallback, // `webImagePickImpl` is called after image is picked on the web - webImagePickImpl: (onImagePickCallback) => webImagePickImpl(http.Client(), ImageFilePicker(), onImagePickCallback), + webImagePickImpl: (onImagePickCallback) => webImagePickImpl(BlocProvider.of(context).state.imageRepository, ImageFilePicker(), onImagePickCallback), // defining the selector (we only want to open the gallery whenever the person wants to upload an image) mediaPickSettingSelector: (context) { diff --git a/test/unit/image_callbacks_test.dart b/test/unit/image_callbacks_test.dart index fea41cb9..8e3ab016 100644 --- a/test/unit/image_callbacks_test.dart +++ b/test/unit/image_callbacks_test.dart @@ -1,12 +1,12 @@ -import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:dartz/dartz.dart'; +import 'package:dwyl_app/data/repositories/repositories.dart'; import 'package:dwyl_app/presentation/widgets/editor/image_callbacks.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; @@ -36,7 +36,7 @@ class FileMock extends MockFile { String get path => 'some_path.png'; } -@GenerateMocks([http.Client, ImageFilePicker, File]) +@GenerateMocks([ImageRepository, ImageFilePicker, File]) void main() { /// Check for context: https://stackoverflow.com/questions/60671728/unable-to-load-assets-in-flutter-tests setUp(() async { @@ -58,7 +58,7 @@ void main() { IOOverrides.runZoned( () async { // Mocks - final clientMock = MockClient(); + final imageRepoMock = MockImageRepository(); final filePickerMock = MockImageFilePicker(); // Set mock behaviour for `filePickerMock` with jpeg magic number byte array https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5 @@ -69,14 +69,12 @@ void main() { // Set mock behaviour for image returned from the picker when(filePickerMock.pickImage()).thenAnswer((_) async => Future.value(FilePickerResult(listMockFiles))); - // Set mock behaviour for `requestMock` - const body = '{"url":"return_url"}'; - final bodyBytes = utf8.encode(body); - when(clientMock.send(any)).thenAnswer((_) async => http.StreamedResponse(Stream>.fromIterable([bodyBytes]), 200)); + // Set mock behaviour for `imageRepoMock` + when(imageRepoMock.uploadImage(any, any)).thenAnswer((_) async => const Right('return_url')); // With the request being "200", we should expect the return URL be the same as the one defined in the mock body. final urlResponse = await webImagePickImpl( - clientMock, + imageRepoMock, filePickerMock, (file) => Future.value(''), ); diff --git a/test/unit/image_callbacks_test.mocks.dart b/test/unit/image_callbacks_test.mocks.dart index c352fd64..74502ace 100644 --- a/test/unit/image_callbacks_test.mocks.dart +++ b/test/unit/image_callbacks_test.mocks.dart @@ -3,15 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; -import 'dart:convert' as _i5; +import 'dart:async' as _i5; +import 'dart:convert' as _i10; import 'dart:io' as _i3; -import 'dart:typed_data' as _i6; +import 'dart:typed_data' as _i7; +import 'package:dartz/dartz.dart' as _i2; +import 'package:dwyl_app/data/repositories/repositories.dart' as _i4; +import 'package:dwyl_app/models/models.dart' as _i6; import 'package:dwyl_app/presentation/widgets/editor/image_callbacks.dart' - as _i7; -import 'package:file_picker/file_picker.dart' as _i8; -import 'package:http/http.dart' as _i2; + as _i8; +import 'package:file_picker/file_picker.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: type=lint @@ -25,8 +27,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { - _FakeResponse_0( +class _FakeEither_0 extends _i1.SmartFake implements _i2.Either { + _FakeEither_0( Object parent, Invocation parentInvocation, ) : super( @@ -35,9 +37,8 @@ class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { ); } -class _FakeStreamedResponse_1 extends _i1.SmartFake - implements _i2.StreamedResponse { - _FakeStreamedResponse_1( +class _FakeFile_1 extends _i1.SmartFake implements _i3.File { + _FakeFile_1( Object parent, Invocation parentInvocation, ) : super( @@ -46,8 +47,8 @@ class _FakeStreamedResponse_1 extends _i1.SmartFake ); } -class _FakeFile_2 extends _i1.SmartFake implements _i3.File { - _FakeFile_2( +class _FakeUri_2 extends _i1.SmartFake implements Uri { + _FakeUri_2( Object parent, Invocation parentInvocation, ) : super( @@ -56,8 +57,8 @@ class _FakeFile_2 extends _i1.SmartFake implements _i3.File { ); } -class _FakeUri_3 extends _i1.SmartFake implements Uri { - _FakeUri_3( +class _FakeDirectory_3 extends _i1.SmartFake implements _i3.Directory { + _FakeDirectory_3( Object parent, Invocation parentInvocation, ) : super( @@ -66,8 +67,8 @@ class _FakeUri_3 extends _i1.SmartFake implements Uri { ); } -class _FakeDirectory_4 extends _i1.SmartFake implements _i3.Directory { - _FakeDirectory_4( +class _FakeDateTime_4 extends _i1.SmartFake implements DateTime { + _FakeDateTime_4( Object parent, Invocation parentInvocation, ) : super( @@ -76,19 +77,9 @@ class _FakeDirectory_4 extends _i1.SmartFake implements _i3.Directory { ); } -class _FakeDateTime_5 extends _i1.SmartFake implements DateTime { - _FakeDateTime_5( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeRandomAccessFile_6 extends _i1.SmartFake +class _FakeRandomAccessFile_5 extends _i1.SmartFake implements _i3.RandomAccessFile { - _FakeRandomAccessFile_6( + _FakeRandomAccessFile_5( Object parent, Invocation parentInvocation, ) : super( @@ -97,8 +88,8 @@ class _FakeRandomAccessFile_6 extends _i1.SmartFake ); } -class _FakeIOSink_7 extends _i1.SmartFake implements _i3.IOSink { - _FakeIOSink_7( +class _FakeIOSink_6 extends _i1.SmartFake implements _i3.IOSink { + _FakeIOSink_6( Object parent, Invocation parentInvocation, ) : super( @@ -107,8 +98,8 @@ class _FakeIOSink_7 extends _i1.SmartFake implements _i3.IOSink { ); } -class _FakeFileStat_8 extends _i1.SmartFake implements _i3.FileStat { - _FakeFileStat_8( +class _FakeFileStat_7 extends _i1.SmartFake implements _i3.FileStat { + _FakeFileStat_7( Object parent, Invocation parentInvocation, ) : super( @@ -117,9 +108,9 @@ class _FakeFileStat_8 extends _i1.SmartFake implements _i3.FileStat { ); } -class _FakeFileSystemEntity_9 extends _i1.SmartFake +class _FakeFileSystemEntity_8 extends _i1.SmartFake implements _i3.FileSystemEntity { - _FakeFileSystemEntity_9( + _FakeFileSystemEntity_8( Object parent, Invocation parentInvocation, ) : super( @@ -128,251 +119,57 @@ class _FakeFileSystemEntity_9 extends _i1.SmartFake ); } -/// A class which mocks [Client]. +/// A class which mocks [ImageRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockClient extends _i1.Mock implements _i2.Client { - MockClient() { +class MockImageRepository extends _i1.Mock implements _i4.ImageRepository { + MockImageRepository() { _i1.throwOnMissingStub(this); } @override - _i4.Future<_i2.Response> head( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #head, - [url], - {#headers: headers}, - ), - returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( - this, - Invocation.method( - #head, - [url], - {#headers: headers}, - ), - )), - ) as _i4.Future<_i2.Response>); - - @override - _i4.Future<_i2.Response> get( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #get, - [url], - {#headers: headers}, - ), - returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( - this, - Invocation.method( - #get, - [url], - {#headers: headers}, - ), - )), - ) as _i4.Future<_i2.Response>); - - @override - _i4.Future<_i2.Response> post( - Uri? url, { - Map? headers, - Object? body, - _i5.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #post, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( - this, - Invocation.method( - #post, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i4.Future<_i2.Response>); - - @override - _i4.Future<_i2.Response> put( - Uri? url, { - Map? headers, - Object? body, - _i5.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #put, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( - this, - Invocation.method( - #put, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i4.Future<_i2.Response>); - - @override - _i4.Future<_i2.Response> patch( - Uri? url, { - Map? headers, - Object? body, - _i5.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #patch, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( - this, - Invocation.method( - #patch, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i4.Future<_i2.Response>); - - @override - _i4.Future<_i2.Response> delete( - Uri? url, { - Map? headers, - Object? body, - _i5.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #delete, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i4.Future<_i2.Response>.value(_FakeResponse_0( - this, - Invocation.method( - #delete, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i4.Future<_i2.Response>); - - @override - _i4.Future read( - Uri? url, { - Map? headers, - }) => + _i5.Future<_i2.Either<_i6.RequestError, String>> uploadImage( + _i7.Uint8List? bytes, + String? filename, + ) => (super.noSuchMethod( Invocation.method( - #read, - [url], - {#headers: headers}, - ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); - - @override - _i4.Future<_i6.Uint8List> readBytes( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #readBytes, - [url], - {#headers: headers}, - ), - returnValue: _i4.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), - ) as _i4.Future<_i6.Uint8List>); - - @override - _i4.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => - (super.noSuchMethod( - Invocation.method( - #send, - [request], + #uploadImage, + [ + bytes, + filename, + ], ), - returnValue: - _i4.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + returnValue: _i5.Future<_i2.Either<_i6.RequestError, String>>.value( + _FakeEither_0<_i6.RequestError, String>( this, Invocation.method( - #send, - [request], + #uploadImage, + [ + bytes, + filename, + ], ), )), - ) as _i4.Future<_i2.StreamedResponse>); - - @override - void close() => super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValueForMissingStub: null, - ); + ) as _i5.Future<_i2.Either<_i6.RequestError, String>>); } /// A class which mocks [ImageFilePicker]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageFilePicker extends _i1.Mock implements _i7.ImageFilePicker { +class MockImageFilePicker extends _i1.Mock implements _i8.ImageFilePicker { MockImageFilePicker() { _i1.throwOnMissingStub(this); } @override - _i4.Future<_i8.FilePickerResult?> pickImage() => (super.noSuchMethod( + _i5.Future<_i9.FilePickerResult?> pickImage() => (super.noSuchMethod( Invocation.method( #pickImage, [], ), - returnValue: _i4.Future<_i8.FilePickerResult?>.value(), - ) as _i4.Future<_i8.FilePickerResult?>); + returnValue: _i5.Future<_i9.FilePickerResult?>.value(), + ) as _i5.Future<_i9.FilePickerResult?>); } /// A class which mocks [File]. @@ -386,7 +183,7 @@ class MockFile extends _i1.Mock implements _i3.File { @override _i3.File get absolute => (super.noSuchMethod( Invocation.getter(#absolute), - returnValue: _FakeFile_2( + returnValue: _FakeFile_1( this, Invocation.getter(#absolute), ), @@ -401,7 +198,7 @@ class MockFile extends _i1.Mock implements _i3.File { @override Uri get uri => (super.noSuchMethod( Invocation.getter(#uri), - returnValue: _FakeUri_3( + returnValue: _FakeUri_2( this, Invocation.getter(#uri), ), @@ -416,14 +213,14 @@ class MockFile extends _i1.Mock implements _i3.File { @override _i3.Directory get parent => (super.noSuchMethod( Invocation.getter(#parent), - returnValue: _FakeDirectory_4( + returnValue: _FakeDirectory_3( this, Invocation.getter(#parent), ), ) as _i3.Directory); @override - _i4.Future<_i3.File> create({ + _i5.Future<_i3.File> create({ bool? recursive = false, bool? exclusive = false, }) => @@ -436,7 +233,7 @@ class MockFile extends _i1.Mock implements _i3.File { #exclusive: exclusive, }, ), - returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + returnValue: _i5.Future<_i3.File>.value(_FakeFile_1( this, Invocation.method( #create, @@ -447,7 +244,7 @@ class MockFile extends _i1.Mock implements _i3.File { }, ), )), - ) as _i4.Future<_i3.File>); + ) as _i5.Future<_i3.File>); @override void createSync({ @@ -467,19 +264,19 @@ class MockFile extends _i1.Mock implements _i3.File { ); @override - _i4.Future<_i3.File> rename(String? newPath) => (super.noSuchMethod( + _i5.Future<_i3.File> rename(String? newPath) => (super.noSuchMethod( Invocation.method( #rename, [newPath], ), - returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + returnValue: _i5.Future<_i3.File>.value(_FakeFile_1( this, Invocation.method( #rename, [newPath], ), )), - ) as _i4.Future<_i3.File>); + ) as _i5.Future<_i3.File>); @override _i3.File renameSync(String? newPath) => (super.noSuchMethod( @@ -487,7 +284,7 @@ class MockFile extends _i1.Mock implements _i3.File { #renameSync, [newPath], ), - returnValue: _FakeFile_2( + returnValue: _FakeFile_1( this, Invocation.method( #renameSync, @@ -497,19 +294,19 @@ class MockFile extends _i1.Mock implements _i3.File { ) as _i3.File); @override - _i4.Future<_i3.File> copy(String? newPath) => (super.noSuchMethod( + _i5.Future<_i3.File> copy(String? newPath) => (super.noSuchMethod( Invocation.method( #copy, [newPath], ), - returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + returnValue: _i5.Future<_i3.File>.value(_FakeFile_1( this, Invocation.method( #copy, [newPath], ), )), - ) as _i4.Future<_i3.File>); + ) as _i5.Future<_i3.File>); @override _i3.File copySync(String? newPath) => (super.noSuchMethod( @@ -517,7 +314,7 @@ class MockFile extends _i1.Mock implements _i3.File { #copySync, [newPath], ), - returnValue: _FakeFile_2( + returnValue: _FakeFile_1( this, Invocation.method( #copySync, @@ -527,13 +324,13 @@ class MockFile extends _i1.Mock implements _i3.File { ) as _i3.File); @override - _i4.Future length() => (super.noSuchMethod( + _i5.Future length() => (super.noSuchMethod( Invocation.method( #length, [], ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override int lengthSync() => (super.noSuchMethod( @@ -545,19 +342,19 @@ class MockFile extends _i1.Mock implements _i3.File { ) as int); @override - _i4.Future lastAccessed() => (super.noSuchMethod( + _i5.Future lastAccessed() => (super.noSuchMethod( Invocation.method( #lastAccessed, [], ), - returnValue: _i4.Future.value(_FakeDateTime_5( + returnValue: _i5.Future.value(_FakeDateTime_4( this, Invocation.method( #lastAccessed, [], ), )), - ) as _i4.Future); + ) as _i5.Future); @override DateTime lastAccessedSync() => (super.noSuchMethod( @@ -565,7 +362,7 @@ class MockFile extends _i1.Mock implements _i3.File { #lastAccessedSync, [], ), - returnValue: _FakeDateTime_5( + returnValue: _FakeDateTime_4( this, Invocation.method( #lastAccessedSync, @@ -575,13 +372,13 @@ class MockFile extends _i1.Mock implements _i3.File { ) as DateTime); @override - _i4.Future setLastAccessed(DateTime? time) => (super.noSuchMethod( + _i5.Future setLastAccessed(DateTime? time) => (super.noSuchMethod( Invocation.method( #setLastAccessed, [time], ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override void setLastAccessedSync(DateTime? time) => super.noSuchMethod( @@ -593,19 +390,19 @@ class MockFile extends _i1.Mock implements _i3.File { ); @override - _i4.Future lastModified() => (super.noSuchMethod( + _i5.Future lastModified() => (super.noSuchMethod( Invocation.method( #lastModified, [], ), - returnValue: _i4.Future.value(_FakeDateTime_5( + returnValue: _i5.Future.value(_FakeDateTime_4( this, Invocation.method( #lastModified, [], ), )), - ) as _i4.Future); + ) as _i5.Future); @override DateTime lastModifiedSync() => (super.noSuchMethod( @@ -613,7 +410,7 @@ class MockFile extends _i1.Mock implements _i3.File { #lastModifiedSync, [], ), - returnValue: _FakeDateTime_5( + returnValue: _FakeDateTime_4( this, Invocation.method( #lastModifiedSync, @@ -623,13 +420,13 @@ class MockFile extends _i1.Mock implements _i3.File { ) as DateTime); @override - _i4.Future setLastModified(DateTime? time) => (super.noSuchMethod( + _i5.Future setLastModified(DateTime? time) => (super.noSuchMethod( Invocation.method( #setLastModified, [time], ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override void setLastModifiedSync(DateTime? time) => super.noSuchMethod( @@ -641,7 +438,7 @@ class MockFile extends _i1.Mock implements _i3.File { ); @override - _i4.Future<_i3.RandomAccessFile> open( + _i5.Future<_i3.RandomAccessFile> open( {_i3.FileMode? mode = _i3.FileMode.read}) => (super.noSuchMethod( Invocation.method( @@ -650,7 +447,7 @@ class MockFile extends _i1.Mock implements _i3.File { {#mode: mode}, ), returnValue: - _i4.Future<_i3.RandomAccessFile>.value(_FakeRandomAccessFile_6( + _i5.Future<_i3.RandomAccessFile>.value(_FakeRandomAccessFile_5( this, Invocation.method( #open, @@ -658,7 +455,7 @@ class MockFile extends _i1.Mock implements _i3.File { {#mode: mode}, ), )), - ) as _i4.Future<_i3.RandomAccessFile>); + ) as _i5.Future<_i3.RandomAccessFile>); @override _i3.RandomAccessFile openSync({_i3.FileMode? mode = _i3.FileMode.read}) => @@ -668,7 +465,7 @@ class MockFile extends _i1.Mock implements _i3.File { [], {#mode: mode}, ), - returnValue: _FakeRandomAccessFile_6( + returnValue: _FakeRandomAccessFile_5( this, Invocation.method( #openSync, @@ -679,7 +476,7 @@ class MockFile extends _i1.Mock implements _i3.File { ) as _i3.RandomAccessFile); @override - _i4.Stream> openRead([ + _i5.Stream> openRead([ int? start, int? end, ]) => @@ -691,13 +488,13 @@ class MockFile extends _i1.Mock implements _i3.File { end, ], ), - returnValue: _i4.Stream>.empty(), - ) as _i4.Stream>); + returnValue: _i5.Stream>.empty(), + ) as _i5.Stream>); @override _i3.IOSink openWrite({ _i3.FileMode? mode = _i3.FileMode.write, - _i5.Encoding? encoding = const _i5.Utf8Codec(), + _i10.Encoding? encoding = const _i10.Utf8Codec(), }) => (super.noSuchMethod( Invocation.method( @@ -708,7 +505,7 @@ class MockFile extends _i1.Mock implements _i3.File { #encoding: encoding, }, ), - returnValue: _FakeIOSink_7( + returnValue: _FakeIOSink_6( this, Invocation.method( #openWrite, @@ -722,37 +519,37 @@ class MockFile extends _i1.Mock implements _i3.File { ) as _i3.IOSink); @override - _i4.Future<_i6.Uint8List> readAsBytes() => (super.noSuchMethod( + _i5.Future<_i7.Uint8List> readAsBytes() => (super.noSuchMethod( Invocation.method( #readAsBytes, [], ), - returnValue: _i4.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), - ) as _i4.Future<_i6.Uint8List>); + returnValue: _i5.Future<_i7.Uint8List>.value(_i7.Uint8List(0)), + ) as _i5.Future<_i7.Uint8List>); @override - _i6.Uint8List readAsBytesSync() => (super.noSuchMethod( + _i7.Uint8List readAsBytesSync() => (super.noSuchMethod( Invocation.method( #readAsBytesSync, [], ), - returnValue: _i6.Uint8List(0), - ) as _i6.Uint8List); + returnValue: _i7.Uint8List(0), + ) as _i7.Uint8List); @override - _i4.Future readAsString( - {_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + _i5.Future readAsString( + {_i10.Encoding? encoding = const _i10.Utf8Codec()}) => (super.noSuchMethod( Invocation.method( #readAsString, [], {#encoding: encoding}, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - String readAsStringSync({_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + String readAsStringSync({_i10.Encoding? encoding = const _i10.Utf8Codec()}) => (super.noSuchMethod( Invocation.method( #readAsStringSync, @@ -763,20 +560,20 @@ class MockFile extends _i1.Mock implements _i3.File { ) as String); @override - _i4.Future> readAsLines( - {_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + _i5.Future> readAsLines( + {_i10.Encoding? encoding = const _i10.Utf8Codec()}) => (super.noSuchMethod( Invocation.method( #readAsLines, [], {#encoding: encoding}, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override List readAsLinesSync( - {_i5.Encoding? encoding = const _i5.Utf8Codec()}) => + {_i10.Encoding? encoding = const _i10.Utf8Codec()}) => (super.noSuchMethod( Invocation.method( #readAsLinesSync, @@ -787,7 +584,7 @@ class MockFile extends _i1.Mock implements _i3.File { ) as List); @override - _i4.Future<_i3.File> writeAsBytes( + _i5.Future<_i3.File> writeAsBytes( List? bytes, { _i3.FileMode? mode = _i3.FileMode.write, bool? flush = false, @@ -801,7 +598,7 @@ class MockFile extends _i1.Mock implements _i3.File { #flush: flush, }, ), - returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + returnValue: _i5.Future<_i3.File>.value(_FakeFile_1( this, Invocation.method( #writeAsBytes, @@ -812,7 +609,7 @@ class MockFile extends _i1.Mock implements _i3.File { }, ), )), - ) as _i4.Future<_i3.File>); + ) as _i5.Future<_i3.File>); @override void writeAsBytesSync( @@ -833,10 +630,10 @@ class MockFile extends _i1.Mock implements _i3.File { ); @override - _i4.Future<_i3.File> writeAsString( + _i5.Future<_i3.File> writeAsString( String? contents, { _i3.FileMode? mode = _i3.FileMode.write, - _i5.Encoding? encoding = const _i5.Utf8Codec(), + _i10.Encoding? encoding = const _i10.Utf8Codec(), bool? flush = false, }) => (super.noSuchMethod( @@ -849,7 +646,7 @@ class MockFile extends _i1.Mock implements _i3.File { #flush: flush, }, ), - returnValue: _i4.Future<_i3.File>.value(_FakeFile_2( + returnValue: _i5.Future<_i3.File>.value(_FakeFile_1( this, Invocation.method( #writeAsString, @@ -861,13 +658,13 @@ class MockFile extends _i1.Mock implements _i3.File { }, ), )), - ) as _i4.Future<_i3.File>); + ) as _i5.Future<_i3.File>); @override void writeAsStringSync( String? contents, { _i3.FileMode? mode = _i3.FileMode.write, - _i5.Encoding? encoding = const _i5.Utf8Codec(), + _i10.Encoding? encoding = const _i10.Utf8Codec(), bool? flush = false, }) => super.noSuchMethod( @@ -884,13 +681,13 @@ class MockFile extends _i1.Mock implements _i3.File { ); @override - _i4.Future exists() => (super.noSuchMethod( + _i5.Future exists() => (super.noSuchMethod( Invocation.method( #exists, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override bool existsSync() => (super.noSuchMethod( @@ -902,13 +699,13 @@ class MockFile extends _i1.Mock implements _i3.File { ) as bool); @override - _i4.Future resolveSymbolicLinks() => (super.noSuchMethod( + _i5.Future resolveSymbolicLinks() => (super.noSuchMethod( Invocation.method( #resolveSymbolicLinks, [], ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override String resolveSymbolicLinksSync() => (super.noSuchMethod( @@ -920,19 +717,19 @@ class MockFile extends _i1.Mock implements _i3.File { ) as String); @override - _i4.Future<_i3.FileStat> stat() => (super.noSuchMethod( + _i5.Future<_i3.FileStat> stat() => (super.noSuchMethod( Invocation.method( #stat, [], ), - returnValue: _i4.Future<_i3.FileStat>.value(_FakeFileStat_8( + returnValue: _i5.Future<_i3.FileStat>.value(_FakeFileStat_7( this, Invocation.method( #stat, [], ), )), - ) as _i4.Future<_i3.FileStat>); + ) as _i5.Future<_i3.FileStat>); @override _i3.FileStat statSync() => (super.noSuchMethod( @@ -940,7 +737,7 @@ class MockFile extends _i1.Mock implements _i3.File { #statSync, [], ), - returnValue: _FakeFileStat_8( + returnValue: _FakeFileStat_7( this, Invocation.method( #statSync, @@ -950,7 +747,7 @@ class MockFile extends _i1.Mock implements _i3.File { ) as _i3.FileStat); @override - _i4.Future<_i3.FileSystemEntity> delete({bool? recursive = false}) => + _i5.Future<_i3.FileSystemEntity> delete({bool? recursive = false}) => (super.noSuchMethod( Invocation.method( #delete, @@ -958,7 +755,7 @@ class MockFile extends _i1.Mock implements _i3.File { {#recursive: recursive}, ), returnValue: - _i4.Future<_i3.FileSystemEntity>.value(_FakeFileSystemEntity_9( + _i5.Future<_i3.FileSystemEntity>.value(_FakeFileSystemEntity_8( this, Invocation.method( #delete, @@ -966,7 +763,7 @@ class MockFile extends _i1.Mock implements _i3.File { {#recursive: recursive}, ), )), - ) as _i4.Future<_i3.FileSystemEntity>); + ) as _i5.Future<_i3.FileSystemEntity>); @override void deleteSync({bool? recursive = false}) => super.noSuchMethod( @@ -979,7 +776,7 @@ class MockFile extends _i1.Mock implements _i3.File { ); @override - _i4.Stream<_i3.FileSystemEvent> watch({ + _i5.Stream<_i3.FileSystemEvent> watch({ int? events = 15, bool? recursive = false, }) => @@ -992,6 +789,6 @@ class MockFile extends _i1.Mock implements _i3.File { #recursive: recursive, }, ), - returnValue: _i4.Stream<_i3.FileSystemEvent>.empty(), - ) as _i4.Stream<_i3.FileSystemEvent>); + returnValue: _i5.Stream<_i3.FileSystemEvent>.empty(), + ) as _i5.Stream<_i3.FileSystemEvent>); } From 46ec0911b4f5ea24c33952a9947cfd3b3852899a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 9 Oct 2023 18:29:16 +0100 Subject: [PATCH 40/43] feat: Adding tests. #275 --- test/bloc/data/data_cubit_test.dart | 15 +++++++++++++++ test/bloc/data/data_state_test.dart | 14 ++++++++++++++ test/unit/core/data_layer_test.dart | 11 +++++++++++ test/unit/models/request_error_test.dart | 14 ++++++++++++++ test/unit/{ => models}/todo_test.dart | 0 test/unit/{ => widgets}/image_callbacks_test.dart | 0 .../{ => widgets}/image_callbacks_test.mocks.dart | 0 7 files changed, 54 insertions(+) create mode 100644 test/bloc/data/data_cubit_test.dart create mode 100644 test/bloc/data/data_state_test.dart create mode 100644 test/unit/core/data_layer_test.dart create mode 100644 test/unit/models/request_error_test.dart rename test/unit/{ => models}/todo_test.dart (100%) rename test/unit/{ => widgets}/image_callbacks_test.dart (100%) rename test/unit/{ => widgets}/image_callbacks_test.mocks.dart (100%) diff --git a/test/bloc/data/data_cubit_test.dart b/test/bloc/data/data_cubit_test.dart new file mode 100644 index 00000000..0dde630b --- /dev/null +++ b/test/bloc/data/data_cubit_test.dart @@ -0,0 +1,15 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dwyl_app/blocs/data/data_cubit.dart'; +import 'package:dwyl_app/data/repositories/repositories.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +void main() { + group('DataCubit', () { + blocTest( + 'emits [] on initial setup', + build: () => DataCubit(imageRepository: ImgupRepository(client: http.Client())), + expect: () => [], + ); + }); +} diff --git a/test/bloc/data/data_state_test.dart b/test/bloc/data/data_state_test.dart new file mode 100644 index 00000000..08eb6178 --- /dev/null +++ b/test/bloc/data/data_state_test.dart @@ -0,0 +1,14 @@ +import 'package:dwyl_app/blocs/data/data_cubit.dart'; +import 'package:dwyl_app/data/repositories/repositories.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +void main() { + group('DataCubit', () { + group('DataInitial', () { + test('supports value comparison', () { + expect(DataInitial(ImgupRepository(client: http.Client())).props, DataInitial(ImgupRepository(client: http.Client())).props); + }); + }); + }); +} diff --git a/test/unit/core/data_layer_test.dart b/test/unit/core/data_layer_test.dart new file mode 100644 index 00000000..7c0d5ef8 --- /dev/null +++ b/test/unit/core/data_layer_test.dart @@ -0,0 +1,11 @@ + +import 'package:dwyl_app/core/data_layer.dart'; +import 'package:dwyl_app/data/repositories/image/image_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Creating data vault works, whilst looking and storing maps - release mode', () { + final dataLayer = createDataLayer(isRelease: true); + expect(dataLayer.lookup(), isA()); + }); +} diff --git a/test/unit/models/request_error_test.dart b/test/unit/models/request_error_test.dart new file mode 100644 index 00000000..cc7a3094 --- /dev/null +++ b/test/unit/models/request_error_test.dart @@ -0,0 +1,14 @@ + +import 'package:dwyl_app/models/errors/errors.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('RequestError defining.', () { + const description = 'Error description'; + + final item = RequestError(code: 400, description: description); + + // Checking attributes + expect(item.description, description); + }); +} diff --git a/test/unit/todo_test.dart b/test/unit/models/todo_test.dart similarity index 100% rename from test/unit/todo_test.dart rename to test/unit/models/todo_test.dart diff --git a/test/unit/image_callbacks_test.dart b/test/unit/widgets/image_callbacks_test.dart similarity index 100% rename from test/unit/image_callbacks_test.dart rename to test/unit/widgets/image_callbacks_test.dart diff --git a/test/unit/image_callbacks_test.mocks.dart b/test/unit/widgets/image_callbacks_test.mocks.dart similarity index 100% rename from test/unit/image_callbacks_test.mocks.dart rename to test/unit/widgets/image_callbacks_test.mocks.dart From 6439db5d922ae5a4a368dfc2bba03040e58db2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Mon, 9 Oct 2023 21:35:15 +0100 Subject: [PATCH 41/43] feat: Adding image repository tests. #275 --- lib/data/providers/image_provider.dart | 14 ++++++- .../widgets/editor/image_callbacks.dart | 9 +++-- test/unit/data/image_repository_test.dart | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 test/unit/data/image_repository_test.dart diff --git a/lib/data/providers/image_provider.dart b/lib/data/providers/image_provider.dart index 8e3c174a..e99ae37e 100644 --- a/lib/data/providers/image_provider.dart +++ b/lib/data/providers/image_provider.dart @@ -6,11 +6,11 @@ import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import 'package:mime/mime.dart'; -const imageEndpoint = 'http://localhost:4000/api/images'; +const imageEndpoint = 'https://imgup.fly.dev/api/images'; /// Image data provider. /// Meant to be invoked by the domain layer (repositories). -/// +/// /// It will call the appropriate URL (in this case, it's `imgup`) to mainly upload images. class ImageProvider { /// HTTP client used to make requests. @@ -20,8 +20,18 @@ class ImageProvider { /// Uploads an image (as an array of [bytes]) to https://imgup.fly.dev/ and returns the appropriate response. Future> uploadImage(Uint8List bytes, String filename) async { + // Check if byte array is empty + if (bytes.isEmpty) { + return Left(RequestError(code: 404, description: 'Empty image. Make sure to select an image with content.')); + } + final request = http.MultipartRequest('POST', Uri.parse(imageEndpoint)); + final mimeType = lookupMimeType('', headerBytes: bytes); + if(mimeType == null) { + return Left(RequestError(code: 404, description: 'Invalid mimetype.')); + } + final httpImage = http.MultipartFile.fromBytes('image', bytes, contentType: MediaType.parse(lookupMimeType('', headerBytes: bytes)!), filename: filename); request.files.add(httpImage); diff --git a/lib/presentation/widgets/editor/image_callbacks.dart b/lib/presentation/widgets/editor/image_callbacks.dart index 9f761553..67eadafe 100644 --- a/lib/presentation/widgets/editor/image_callbacks.dart +++ b/lib/presentation/widgets/editor/image_callbacks.dart @@ -2,14 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:dwyl_app/data/repositories/repositories.dart'; +import 'package:dwyl_app/logging/logging.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -/// The URL endpoint in which the images will be uploaded and hosted. -const apiEndpointURL = 'http://localhost:4000/api/images'; - /// Receives a file [file], copies it to the app's documents directory and returns the path of the copied file. Future onImagePickCallback(File file) async { final appDocDir = await getApplicationDocumentsDirectory(); @@ -40,7 +38,10 @@ Future webImagePickImpl(ImageRepository imageRepository, ImageFilePicke final uploadRet = await imageRepository.uploadImage(bytes, platformFile.name); return uploadRet.fold( - (error) => null, + (error) { + logError('Error uploading image: \n${error.toString()}'); + return null; + }, (r) => r, ); } diff --git a/test/unit/data/image_repository_test.dart b/test/unit/data/image_repository_test.dart new file mode 100644 index 00000000..4dd3142a --- /dev/null +++ b/test/unit/data/image_repository_test.dart @@ -0,0 +1,40 @@ +import 'dart:typed_data'; + +import 'package:dwyl_app/core/data_layer.dart'; +import 'package:dwyl_app/data/repositories/image/image_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +void main() { + test('ImageRepository should yield results on a successful request (these make a REAL request)', () async { + final imageRepository = ImgupRepository(client: http.Client()); + + // Byte array from number byte array https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5. + final bytes = Uint8List.fromList([0xff, 0xd8, 0xff, 0xe0]); + + // Should return an URL, not an error. + final ret = await imageRepository.uploadImage(bytes, "cool_birthday"); + expect(ret.isRight(), true); + }); + + test('ImageRepository should yield an error because bytes array is empty.', () async { + final imageRepository = ImgupRepository(client: http.Client()); + + final bytes = Uint8List.fromList([]); + + // Should error out + final ret = await imageRepository.uploadImage(bytes, "cool_birthday"); + expect(ret.isLeft(), true); + }); + + test('ImageRepository should yield an error because bytes array has invalid mimetype', () async { + final imageRepository = ImgupRepository(client: http.Client()); + + // Invalid byte array (a simple [0] does not have any mime type) + final bytes = Uint8List.fromList([0]); + + // Should error out + final ret = await imageRepository.uploadImage(bytes, "cool_birthday"); + expect(ret.isLeft(), true); + }); +} From d6d300ec9ddfa0d454eef70ff76175c7b8cc5680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Tue, 10 Oct 2023 14:45:59 +0100 Subject: [PATCH 42/43] fix: Formatting. #275 --- test/unit/data/image_repository_test.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/unit/data/image_repository_test.dart b/test/unit/data/image_repository_test.dart index 4dd3142a..90ecbd98 100644 --- a/test/unit/data/image_repository_test.dart +++ b/test/unit/data/image_repository_test.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import 'package:dwyl_app/core/data_layer.dart'; import 'package:dwyl_app/data/repositories/image/image_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; @@ -13,7 +12,7 @@ void main() { final bytes = Uint8List.fromList([0xff, 0xd8, 0xff, 0xe0]); // Should return an URL, not an error. - final ret = await imageRepository.uploadImage(bytes, "cool_birthday"); + final ret = await imageRepository.uploadImage(bytes, 'cool_birthday'); expect(ret.isRight(), true); }); @@ -23,18 +22,18 @@ void main() { final bytes = Uint8List.fromList([]); // Should error out - final ret = await imageRepository.uploadImage(bytes, "cool_birthday"); + final ret = await imageRepository.uploadImage(bytes, 'cool_birthday'); expect(ret.isLeft(), true); }); - test('ImageRepository should yield an error because bytes array has invalid mimetype', () async { + test('ImageRepository should yield an error because bytes array has invalid mimetype', () async { final imageRepository = ImgupRepository(client: http.Client()); // Invalid byte array (a simple [0] does not have any mime type) final bytes = Uint8List.fromList([0]); // Should error out - final ret = await imageRepository.uploadImage(bytes, "cool_birthday"); + final ret = await imageRepository.uploadImage(bytes, 'cool_birthday'); expect(ret.isLeft(), true); }); } From 75e373dc2cb4999ddf98d577e0e379f93a61e724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Arteiro?= Date: Wed, 11 Oct 2023 09:40:48 +0100 Subject: [PATCH 43/43] chore: Fix Flutter analyze problem with importing bloc in Data Cubit. #275 --- lib/blocs/data/data_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blocs/data/data_cubit.dart b/lib/blocs/data/data_cubit.dart index 20d72c59..c6ed8429 100644 --- a/lib/blocs/data/data_cubit.dart +++ b/lib/blocs/data/data_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dwyl_app/data/repositories/repositories.dart'; import 'package:equatable/equatable.dart';